Commit 2ab774d7 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: real bot chess games via Stockfish + interactive game viewer

Part A - Bot Game Engine (BotGamePlayerService):
- Maps player ratings to 7 bot personalities (amina→grandmaster)
- Plays actual chess games move-by-move via Stockfish API
- Full FEN tracking, UCI→SAN conversion, PGN generation
- Game termination: checkmate, stalemate, 50-move, repetition, 80-move adjudication
- Stores complete games in existing `matches` table (pgn, moves JSONB, fen)

Part B - Game Viewer:
- Interactive chessboard (CSS grid + Unicode pieces)
- Move-by-move navigation (first/prev/play/next/last + keyboard arrows)
- Move list with click-to-jump
- Last move highlighting
- Auto-play mode (800ms per move)
- Uses chess.js for position replay from UCI moves

Integration:
- "لعب مباريات حقيقية" checkbox in bot testing panel
- "♟ عرض" (View Game) button next to each completed pairing with match_id
- Route: /tournaments/{id}/game/{matchId}
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent c2073097
......@@ -95,6 +95,9 @@ return [
'tournaments/{id}/players/import' => ['module' => 'tournaments', 'action' => 'importPlayers'],
'tournaments/{id}/players/{playerId}/withdraw' => ['module' => 'tournaments', 'action' => 'withdrawPlayer'],
// Tournaments - Game Viewer
'tournaments/{id}/game/{matchId}' => ['module' => 'tournaments', 'action' => 'viewGame'],
// Tournaments - Bot Testing
'tournaments/{id}/bots/populate' => ['module' => 'tournaments', 'action' => 'populateBots'],
'tournaments/{id}/bots/cleanup' => ['module' => 'tournaments', 'action' => 'cleanupBots'],
......
......@@ -1224,10 +1224,30 @@ class TournamentsController
$tournamentId = $params['id'];
$drawRate = (float)($_POST['draw_rate'] ?? 15) / 100;
$drawRate = max(0, min(0.5, $drawRate));
$playActual = !empty($_POST['play_actual']);
require_once __DIR__ . '/services/BotSimulationService.php';
$result = BotSimulationService::simulateNextRound($tournamentId, $drawRate);
$result = BotSimulationService::simulateNextRound($tournamentId, $drawRate, $playActual);
Response::json($result);
}
public function viewGame(array $params, string $method): void
{
$tournamentId = $params['id'];
$matchId = $params['matchId'];
$game = $this->db->selectOne('matches', ['id' => "eq.{$matchId}"]);
if (!$game) {
http_response_code(404);
View::render('errors/404');
return;
}
$tournament = $this->db->selectOne('el3ab_tournaments', ['id' => "eq.{$tournamentId}"]);
$pageTitle = 'عرض المباراة';
$moduleCSS = 'tournaments';
View::render('tournaments/game', compact('game', 'tournament', 'pageTitle', 'moduleCSS'));
}
}
<?php
class BotGamePlayerService
{
private static array $ratingBotMap = [
[0, 700, 'amina'],
[700, 1000, 'tarek'],
[1000, 1200, 'nour'],
[1200, 1400, 'omar'],
[1400, 1600, 'layla'],
[1600, 1800, 'ziad'],
[1800, 9999, 'grandmaster'],
];
private static array $timeControlMs = [
'bullet_1_0' => 60000, 'bullet_1_1' => 60000, 'bullet_2_1' => 120000,
'blitz_3_0' => 180000, 'blitz_3_2' => 180000, 'blitz_5_0' => 300000, 'blitz_5_3' => 300000,
'rapid_10_0' => 600000, 'rapid_10_5' => 600000, 'rapid_15_10' => 900000, 'rapid_30_0' => 1800000,
'classical_60_0' => 3600000, 'classical_90_30' => 5400000, 'custom' => 600000,
];
public static function mapRatingToBot(int $rating): string
{
foreach (self::$ratingBotMap as [$min, $max, $botId]) {
if ($rating >= $min && $rating < $max) {
return $botId;
}
}
return 'nour';
}
public static function playGame(
string $tournamentId,
int $roundNumber,
string $pairingId,
string $whiteName,
int $whiteRating,
string $blackName,
int $blackRating,
string $timeControl
): array {
$db = Database::getInstance();
$whiteBotId = self::mapRatingToBot($whiteRating);
$blackBotId = self::mapRatingToBot($blackRating);
$initialTimeMs = self::$timeControlMs[$timeControl] ?? 600000;
$matchData = [
'game_key' => 'chess',
'match_type' => 'tournament',
'tournament_id' => $tournamentId,
'tournament_round' => $roundNumber,
'status' => 'in_progress',
'time_control' => $timeControl,
'initial_time_ms' => $initialTimeMs,
'increment_ms' => 0,
'bot_id' => $whiteBotId,
'bot_difficulty' => $blackBotId,
'white_rating_before' => $whiteRating,
'black_rating_before' => $blackRating,
'is_rated' => false,
'started_at' => date('c'),
];
$record = $db->insert('matches', $matchData);
$matchId = $record['id'] ?? null;
if (!$matchId) {
return ['success' => false, 'error' => 'Failed to create match record'];
}
$fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
$moves = [];
$positionHashes = [$fen => 1];
$moveCount = 0;
$lastEval = 0.0;
$result = null;
$resultReason = null;
$maxMoves = 80;
$sanMoves = [];
while ($moveCount < $maxMoves) {
$sideToMove = self::getSideToMove($fen);
$botId = ($sideToMove === 'w') ? $whiteBotId : $blackBotId;
$response = ApiProxy::stockfish('POST', '/api/chess/move', [
'fen' => $fen,
'bot_id' => $botId,
]);
if ($response['status'] !== 200 || empty($response['body']['best_move'])) {
if ($sideToMove === 'w') {
$result = 'black_wins';
} else {
$result = 'white_wins';
}
$resultReason = 'checkmate';
break;
}
$uciMove = $response['body']['best_move'];
$eval = $response['body']['evaluation'] ?? 0;
$thinkTimeMs = $response['body']['think_time_ms'] ?? 0;
$lastEval = ($sideToMove === 'w') ? $eval : -$eval;
$san = self::uciToSan($fen, $uciMove);
$newFen = self::applyUciMove($fen, $uciMove);
if (!$newFen) {
$result = 'aborted';
$resultReason = 'invalid_move';
break;
}
$moves[] = [
'from' => substr($uciMove, 0, 2),
'to' => substr($uciMove, 2, 2),
'san' => $san,
'uci' => $uciMove,
'eval' => $eval,
'fen_after' => $newFen,
'time_ms' => $thinkTimeMs,
];
$sanMoves[] = $san;
$moveCount++;
$fen = $newFen;
$posKey = explode(' ', $fen);
$posHash = $posKey[0] . ' ' . $posKey[1] . ' ' . $posKey[2] . ' ' . $posKey[3];
$positionHashes[$posHash] = ($positionHashes[$posHash] ?? 0) + 1;
if ($positionHashes[$posHash] >= 3) {
$result = 'threefold_repetition';
$resultReason = 'repetition';
break;
}
$halfmoveClock = (int)($posKey[4] ?? 0);
if ($halfmoveClock >= 100) {
$result = 'fifty_moves';
$resultReason = '50_move_rule';
break;
}
if (self::isStalemate($newFen)) {
$result = 'stalemate';
$resultReason = 'stalemate';
break;
}
}
if ($result === null && $moveCount >= $maxMoves) {
if ($lastEval > 3.0) {
$result = 'white_wins';
} elseif ($lastEval < -3.0) {
$result = 'black_wins';
} else {
$result = 'draw';
}
$resultReason = 'adjudication';
}
$pgn = self::buildPgn($sanMoves, $whiteName, $blackName, $result);
$db->update('matches', ['id' => "eq.{$matchId}"], [
'status' => 'completed',
'result' => $result,
'current_fen' => $fen,
'pgn' => $pgn,
'moves' => json_encode($moves),
'move_count' => $moveCount,
'game_state' => json_encode(['result_reason' => $resultReason, 'white_bot' => $whiteBotId, 'black_bot' => $blackBotId]),
'completed_at' => date('c'),
]);
return [
'success' => true,
'match_id' => $matchId,
'result' => $result,
'move_count' => $moveCount,
'result_reason' => $resultReason,
];
}
private static function getSideToMove(string $fen): string
{
$parts = explode(' ', $fen);
return $parts[1] ?? 'w';
}
private static function isStalemate(string $fen): bool
{
$response = ApiProxy::stockfish('POST', '/api/chess/move', [
'fen' => $fen,
'bot_id' => 'grandmaster',
]);
return $response['status'] !== 200 || empty($response['body']['best_move']);
}
private static function uciToSan(string $fen, string $uciMove): string
{
$from = substr($uciMove, 0, 2);
$to = substr($uciMove, 2, 2);
$promotion = strlen($uciMove) > 4 ? strtoupper(substr($uciMove, 4, 1)) : '';
$board = self::fenToBoard($fen);
$piece = $board[$from] ?? '';
$pieceType = strtolower($piece);
$targetPiece = $board[$to] ?? '';
$isCapture = !empty($targetPiece);
if ($pieceType === 'k') {
if ($from === 'e1' && $to === 'g1') return 'O-O';
if ($from === 'e1' && $to === 'c1') return 'O-O-O';
if ($from === 'e8' && $to === 'g8') return 'O-O';
if ($from === 'e8' && $to === 'c8') return 'O-O-O';
}
$fenParts = explode(' ', $fen);
$enPassant = $fenParts[3] ?? '-';
if ($pieceType === 'p' && $to === $enPassant) {
$isCapture = true;
}
$san = '';
if ($pieceType === 'p') {
if ($isCapture) {
$san = $from[0] . 'x' . $to;
} else {
$san = $to;
}
if ($promotion) {
$san .= '=' . $promotion;
}
} else {
$san = strtoupper($pieceType);
$san .= ($isCapture ? 'x' : '') . $to;
}
return $san;
}
private static function fenToBoard(string $fen): array
{
$board = [];
$parts = explode(' ', $fen);
$rows = explode('/', $parts[0]);
$ranks = ['8', '7', '6', '5', '4', '3', '2', '1'];
$files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
foreach ($rows as $rankIdx => $row) {
$fileIdx = 0;
for ($i = 0; $i < strlen($row); $i++) {
$ch = $row[$i];
if (is_numeric($ch)) {
$fileIdx += (int)$ch;
} else {
$square = $files[$fileIdx] . $ranks[$rankIdx];
$board[$square] = $ch;
$fileIdx++;
}
}
}
return $board;
}
public static function applyUciMove(string $fen, string $uciMove): ?string
{
$parts = explode(' ', $fen);
$boardStr = $parts[0];
$side = $parts[1];
$castling = $parts[2];
$enPassant = $parts[3];
$halfmove = (int)$parts[4];
$fullmove = (int)$parts[5];
$board = self::fenToBoard($fen);
$from = substr($uciMove, 0, 2);
$to = substr($uciMove, 2, 2);
$promotion = strlen($uciMove) > 4 ? substr($uciMove, 4, 1) : null;
$piece = $board[$from] ?? null;
if (!$piece) return null;
$capturedPiece = $board[$to] ?? null;
$pieceType = strtolower($piece);
unset($board[$from]);
if ($pieceType === 'p' && $to === $enPassant) {
$epRank = ($side === 'w') ? ((int)$to[1] - 1) : ((int)$to[1] + 1);
$epSquare = $to[0] . $epRank;
unset($board[$epSquare]);
$capturedPiece = 'p';
}
if ($promotion) {
$board[$to] = ($side === 'w') ? strtoupper($promotion) : $promotion;
} else {
$board[$to] = $piece;
}
if ($pieceType === 'k') {
if ($from === 'e1' && $to === 'g1') {
$board['f1'] = $board['h1'] ?? 'R';
unset($board['h1']);
} elseif ($from === 'e1' && $to === 'c1') {
$board['d1'] = $board['a1'] ?? 'R';
unset($board['a1']);
} elseif ($from === 'e8' && $to === 'g8') {
$board['f8'] = $board['h8'] ?? 'r';
unset($board['h8']);
} elseif ($from === 'e8' && $to === 'c8') {
$board['d8'] = $board['a8'] ?? 'r';
unset($board['a8']);
}
}
$newCastling = $castling;
if ($from === 'e1' || $from === 'h1' || $to === 'h1') $newCastling = str_replace('K', '', $newCastling);
if ($from === 'e1' || $from === 'a1' || $to === 'a1') $newCastling = str_replace('Q', '', $newCastling);
if ($from === 'e8' || $from === 'h8' || $to === 'h8') $newCastling = str_replace('k', '', $newCastling);
if ($from === 'e8' || $from === 'a8' || $to === 'a8') $newCastling = str_replace('q', '', $newCastling);
if ($newCastling === '') $newCastling = '-';
$newEnPassant = '-';
if ($pieceType === 'p') {
$fromRank = (int)$from[1];
$toRank = (int)$to[1];
if (abs($toRank - $fromRank) === 2) {
$epRank = ($fromRank + $toRank) / 2;
$newEnPassant = $from[0] . (int)$epRank;
}
}
$newHalfmove = ($pieceType === 'p' || $capturedPiece) ? 0 : $halfmove + 1;
$newFullmove = ($side === 'b') ? $fullmove + 1 : $fullmove;
$newSide = ($side === 'w') ? 'b' : 'w';
$newBoardStr = self::boardToFen($board);
return "{$newBoardStr} {$newSide} {$newCastling} {$newEnPassant} {$newHalfmove} {$newFullmove}";
}
private static function boardToFen(array $board): string
{
$ranks = ['8', '7', '6', '5', '4', '3', '2', '1'];
$files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
$rows = [];
foreach ($ranks as $rank) {
$row = '';
$empty = 0;
foreach ($files as $file) {
$square = $file . $rank;
if (isset($board[$square])) {
if ($empty > 0) {
$row .= $empty;
$empty = 0;
}
$row .= $board[$square];
} else {
$empty++;
}
}
if ($empty > 0) $row .= $empty;
$rows[] = $row;
}
return implode('/', $rows);
}
private static function buildPgn(array $sanMoves, string $white, string $black, string $result): string
{
$resultStr = match($result) {
'white_wins' => '1-0',
'black_wins' => '0-1',
'draw', 'stalemate', 'threefold_repetition', 'fifty_moves', 'insufficient_material' => '1/2-1/2',
default => '*',
};
$pgn = "[White \"{$white}\"]\n[Black \"{$black}\"]\n[Result \"{$resultStr}\"]\n\n";
for ($i = 0; $i < count($sanMoves); $i += 2) {
$moveNum = intdiv($i, 2) + 1;
$pgn .= "{$moveNum}. {$sanMoves[$i]}";
if (isset($sanMoves[$i + 1])) {
$pgn .= " {$sanMoves[$i + 1]} ";
} else {
$pgn .= ' ';
}
}
$pgn .= $resultStr;
return $pgn;
}
}
......@@ -188,7 +188,7 @@ class BotSimulationService
return ['success' => true, 'count' => $inserted];
}
public static function autoPlayRound(string $tournamentId, string $roundId, float $drawRate = 0.15): array
public static function autoPlayRound(string $tournamentId, string $roundId, float $drawRate = 0.15, bool $playActual = false): array
{
$db = Database::getInstance();
......@@ -249,8 +249,27 @@ class BotSimulationService
$ratingWhite = $ratingsMap[$whiteId] ?? 1200;
$ratingBlack = $ratingsMap[$blackId] ?? 1200;
$result = self::simulateResult($ratingWhite, $ratingBlack, $drawRate);
$results[] = ['pairingId' => $pairingId, 'result' => $result, 'whiteName' => $whiteName, 'blackName' => $blackName];
$matchId = null;
if ($playActual) {
require_once __DIR__ . '/BotGamePlayerService.php';
$gameResult = BotGamePlayerService::playGame(
$tournamentId, $round['round_number'] ?? 1, $pairingId,
$whiteName, $ratingWhite, $blackName, $ratingBlack,
$tournament['time_control'] ?? 'rapid_10_0'
);
if ($gameResult['success']) {
$result = $gameResult['result'];
$matchId = $gameResult['match_id'];
} else {
$result = self::simulateResult($ratingWhite, $ratingBlack, $drawRate);
}
} else {
$result = self::simulateResult($ratingWhite, $ratingBlack, $drawRate);
}
$entry = ['pairingId' => $pairingId, 'result' => $result, 'whiteName' => $whiteName, 'blackName' => $blackName];
if ($matchId) $entry['match_id'] = $matchId;
$results[] = $entry;
if ($result === 'white_wins') $summary['white_wins']++;
elseif ($result === 'black_wins') $summary['black_wins']++;
......@@ -290,7 +309,7 @@ class BotSimulationService
return 'black_wins';
}
public static function simulateNextRound(string $tournamentId, float $drawRate = 0.15): array
public static function simulateNextRound(string $tournamentId, float $drawRate = 0.15, bool $playActual = false): array
{
$db = Database::getInstance();
......@@ -355,7 +374,7 @@ class BotSimulationService
}
if ($roundId) {
$playResult = self::autoPlayRound($tournamentId, $roundId, $drawRate);
$playResult = self::autoPlayRound($tournamentId, $roundId, $drawRate, $playActual);
if (!$playResult['success']) {
return $playResult;
}
......
......@@ -107,6 +107,10 @@ if (!empty($rounds)) {
<label class="form-label">نسبة التعادل %</label>
<input type="number" id="simDrawRate" class="form-input" value="15" min="0" max="50" step="5">
</div>
<label class="checkbox-label" style="display:flex;align-items:center;gap:8px;padding-top:20px;">
<input type="checkbox" id="playActualCheckbox">
<span style="font-size:13px;">لعب مباريات حقيقية بالمحرك (أبطأ ≈ ٥ دقائق/جولة)</span>
</label>
<button type="button" class="btn btn-primary" id="simulateBtn" onclick="simulateFullTournament()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
محاكاة البطولة بالكامل
......@@ -155,21 +159,28 @@ function autoPlayCurrentRound() {
function simulateFullTournament() {
const drawRate = document.getElementById('simDrawRate').value;
const playActual = document.getElementById('playActualCheckbox').checked;
const totalRounds = <?= $totalRounds ?>;
if (!confirm('محاكاة كل الجولات المتبقية؟ هذا قد يستغرق بضع ثوانٍ.')) return;
const msg = playActual
? 'محاكاة بمباريات حقيقية — قد يستغرق ٥-١٠ دقائق لكل جولة. متأكد؟'
: 'محاكاة كل الجولات المتبقية؟ هذا قد يستغرق بضع ثوانٍ.';
if (!confirm(msg)) return;
document.getElementById('simProgress').style.display = 'block';
document.getElementById('simulateBtn').disabled = true;
runNextRound(drawRate, totalRounds);
runNextRound(drawRate, totalRounds, playActual);
}
function runNextRound(drawRate, totalRounds) {
function runNextRound(drawRate, totalRounds, playActual) {
let body = '_csrf=<?= Auth::csrfToken() ?>&draw_rate=' + drawRate;
if (playActual) body += '&play_actual=1';
fetch('/tournaments/<?= $tournament['id'] ?>/simulate-round', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: '_csrf=<?= Auth::csrfToken() ?>&draw_rate=' + drawRate
body: body
})
.then(r => r.json())
.then(data => {
......@@ -187,7 +198,7 @@ function runNextRound(drawRate, totalRounds) {
document.getElementById('simStatusText').textContent = 'اكتملت المحاكاة!';
setTimeout(() => location.reload(), 1500);
} else {
setTimeout(() => runNextRound(drawRate, totalRounds), 500);
setTimeout(() => runNextRound(drawRate, totalRounds, playActual), 500);
}
})
.catch(e => {
......
<?php
$moves = $game['moves'] ?? [];
if (is_string($moves)) $moves = json_decode($moves, true) ?? [];
$gameState = $game['game_state'] ?? [];
if (is_string($gameState)) $gameState = json_decode($gameState, true) ?? [];
$whiteName = $gameState['white_bot'] ?? $game['bot_id'] ?? 'أبيض';
$blackName = $gameState['black_bot'] ?? $game['bot_difficulty'] ?? 'أسود';
$resultStr = match($game['result'] ?? '') {
'white_wins' => '1-0',
'black_wins' => '0-1',
'draw', 'stalemate', 'threefold_repetition', 'fifty_moves' => '½-½',
default => '*',
};
?>
<div class="content-header">
<div>
<h1>عرض المباراة</h1>
<?php if ($tournament): ?>
<a href="/tournaments/<?= $tournament['id'] ?>?tab=rounds" class="btn btn-ghost btn-sm">← العودة للبطولة</a>
<?php endif; ?>
</div>
</div>
<div class="game-viewer-container" style="display:grid;grid-template-columns:1fr 300px;gap:24px;max-width:900px;">
<!-- Board -->
<div>
<div class="game-header" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:12px;background:var(--bg-secondary);border-radius:8px;">
<div>
<strong style="color:var(--text-primary);"><?= View::e($game['white_rating_before'] ?? '?') ?></strong>
<span class="text-muted" style="margin:0 8px;">vs</span>
<strong style="color:var(--text-primary);"><?= View::e($game['black_rating_before'] ?? '?') ?></strong>
</div>
<span class="badge <?= $game['result'] === 'white_wins' ? 'badge-success' : ($game['result'] === 'black_wins' ? 'badge-danger' : 'badge-warning') ?>">
<?= $resultStr ?>
</span>
<span class="text-muted"><?= $game['move_count'] ?? 0 ?> نقلة</span>
</div>
<div id="chessBoard" style="width:100%;aspect-ratio:1;display:grid;grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(8,1fr);border:2px solid var(--border-color);border-radius:4px;overflow:hidden;font-size:0;direction:ltr;"></div>
<div style="display:flex;align-items:center;justify-content:center;gap:12px;margin-top:16px;">
<button class="btn btn-ghost btn-sm" onclick="gameViewer.goTo(0)" title="البداية"></button>
<button class="btn btn-ghost btn-sm" onclick="gameViewer.prev()" title="السابق"></button>
<button class="btn btn-primary btn-sm" onclick="gameViewer.togglePlay()" id="playBtn" title="تشغيل"></button>
<button class="btn btn-ghost btn-sm" onclick="gameViewer.next()" title="التالي"></button>
<button class="btn btn-ghost btn-sm" onclick="gameViewer.goTo(-1)" title="النهاية"></button>
<span class="text-muted text-sm" id="moveCounter" style="margin-right:12px;">0 / <?= $game['move_count'] ?? 0 ?></span>
</div>
</div>
<!-- Move List -->
<div class="card" style="max-height:500px;overflow-y:auto;">
<div class="card-header" style="position:sticky;top:0;z-index:1;background:var(--bg-card);">
<h3 class="card-title" style="font-size:14px;">النقلات</h3>
</div>
<div id="moveList" style="padding:8px;font-family:monospace;font-size:13px;direction:ltr;"></div>
</div>
</div>
<script src="/public/js/chess.min.js"></script>
<script>
const PIECES_UNICODE = {
'K': '♔', 'Q': '♕', 'R': '♖', 'B': '♗', 'N': '♘', 'P': '♙',
'k': '♚', 'q': '♛', 'r': '♜', 'b': '♝', 'n': '♞', 'p': '♟'
};
const gameViewer = {
chess: null,
moves: <?= json_encode(array_map(fn($m) => ['from' => $m['from'], 'to' => $m['to'], 'san' => $m['san'] ?? '', 'promotion' => (strlen($m['uci'] ?? '') > 4 ? substr($m['uci'], 4, 1) : null)], $moves)) ?>,
positions: [],
currentPos: 0,
playing: false,
playInterval: null,
init() {
this.chess = new Chess();
this.positions = [this.chess.fen()];
for (const move of this.moves) {
const result = this.chess.move({from: move.from, to: move.to, promotion: move.promotion || 'q'});
if (!result) break;
this.positions.push(this.chess.fen());
}
this.currentPos = this.positions.length - 1;
this.render();
this.buildMoveList();
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') this.prev();
if (e.key === 'ArrowRight') this.next();
if (e.key === 'Home') this.goTo(0);
if (e.key === 'End') this.goTo(-1);
});
},
render() {
const fen = this.positions[this.currentPos];
const board = document.getElementById('chessBoard');
board.innerHTML = '';
const rows = fen.split(' ')[0].split('/');
for (let rank = 0; rank < 8; rank++) {
let file = 0;
for (let i = 0; i < rows[rank].length; i++) {
const ch = rows[rank][i];
if ('12345678'.includes(ch)) {
for (let e = 0; e < parseInt(ch); e++) {
board.appendChild(this.createSquare(rank, file++, null));
}
} else {
board.appendChild(this.createSquare(rank, file++, ch));
}
}
}
document.getElementById('moveCounter').textContent = `${this.currentPos} / ${this.positions.length - 1}`;
this.highlightActiveMove();
},
createSquare(rank, file, piece) {
const sq = document.createElement('div');
const isLight = (rank + file) % 2 === 0;
sq.style.cssText = `display:flex;align-items:center;justify-content:center;font-size:clamp(24px,4.5vw,48px);background:${isLight ? '#E8D5B5' : '#B58863'};cursor:default;user-select:none;`;
const lastMove = this.currentPos > 0 ? this.moves[this.currentPos - 1] : null;
if (lastMove) {
const fromFile = lastMove.from.charCodeAt(0) - 97;
const fromRank = 8 - parseInt(lastMove.from[1]);
const toFile = lastMove.to.charCodeAt(0) - 97;
const toRank = 8 - parseInt(lastMove.to[1]);
if ((rank === fromRank && file === fromFile) || (rank === toRank && file === toFile)) {
sq.style.background = isLight ? '#F5F682' : '#BBCC44';
}
}
if (piece) {
sq.textContent = PIECES_UNICODE[piece] || '';
}
return sq;
},
buildMoveList() {
const list = document.getElementById('moveList');
list.innerHTML = '';
for (let i = 0; i < this.moves.length; i += 2) {
const moveNum = Math.floor(i / 2) + 1;
const row = document.createElement('div');
row.style.cssText = 'display:flex;gap:4px;padding:2px 4px;border-radius:4px;';
const numSpan = document.createElement('span');
numSpan.style.cssText = 'width:28px;color:var(--text-muted);text-align:right;';
numSpan.textContent = moveNum + '.';
const whiteSpan = document.createElement('span');
whiteSpan.style.cssText = 'flex:1;cursor:pointer;padding:2px 4px;border-radius:3px;';
whiteSpan.textContent = this.moves[i]?.san || '';
whiteSpan.dataset.pos = i + 1;
whiteSpan.onclick = () => this.goTo(i + 1);
row.appendChild(numSpan);
row.appendChild(whiteSpan);
if (this.moves[i + 1]) {
const blackSpan = document.createElement('span');
blackSpan.style.cssText = 'flex:1;cursor:pointer;padding:2px 4px;border-radius:3px;';
blackSpan.textContent = this.moves[i + 1]?.san || '';
blackSpan.dataset.pos = i + 2;
blackSpan.onclick = () => this.goTo(i + 2);
row.appendChild(blackSpan);
}
list.appendChild(row);
}
},
highlightActiveMove() {
document.querySelectorAll('#moveList span[data-pos]').forEach(s => {
s.style.background = parseInt(s.dataset.pos) === this.currentPos ? 'var(--brand-blue)' : '';
s.style.color = parseInt(s.dataset.pos) === this.currentPos ? '#fff' : '';
});
},
goTo(pos) {
if (pos === -1) pos = this.positions.length - 1;
this.currentPos = Math.max(0, Math.min(pos, this.positions.length - 1));
this.render();
},
next() { this.goTo(this.currentPos + 1); },
prev() { this.goTo(this.currentPos - 1); },
togglePlay() {
if (this.playing) {
clearInterval(this.playInterval);
this.playing = false;
document.getElementById('playBtn').textContent = '▶';
} else {
if (this.currentPos >= this.positions.length - 1) this.currentPos = 0;
this.playing = true;
document.getElementById('playBtn').textContent = '⏸';
this.playInterval = setInterval(() => {
if (this.currentPos >= this.positions.length - 1) {
this.togglePlay();
return;
}
this.next();
}, 800);
}
}
};
document.addEventListener('DOMContentLoaded', () => gameViewer.init());
</script>
......@@ -348,6 +348,11 @@ $tabs['arbiter'] = 'أدوات الحكم';
<td class="font-medium"><?= View::e($r['whiteName'] ?? $r['white_name'] ?? '-') ?></td>
<td><span class="badge <?= $resultClass ?>"><?= $resultLabel ?></span></td>
<td class="font-medium"><?= View::e($r['blackName'] ?? $r['black_name'] ?? '-') ?></td>
<td>
<?php if (!empty($r['match_id'])): ?>
<a href="/tournaments/<?= $tournament['id'] ?>/game/<?= $r['match_id'] ?>" class="btn btn-xs btn-ghost">♟ عرض</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
......
var Chess=function(r){var u="b",s="w",l=-1,_="p",A="n",S="b",m="r",y="q",p="k",t="pnbrqkPNBRQK",e="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",g=["1-0","0-1","1/2-1/2","*"],C={b:[16,32,17,15],w:[-16,-32,-17,-15]},T={n:[-18,-33,-31,-14,18,33,31,14],b:[-17,-15,17,15],r:[-16,1,16,-1],q:[-17,-16,-15,1,17,16,15,-1],k:[-17,-16,-15,1,17,16,15,-1]},c=[20,0,0,0,0,0,0,24,0,0,0,0,0,0,20,0,0,20,0,0,0,0,0,24,0,0,0,0,0,20,0,0,0,0,20,0,0,0,0,24,0,0,0,0,20,0,0,0,0,0,0,20,0,0,0,24,0,0,0,20,0,0,0,0,0,0,0,0,20,0,0,24,0,0,20,0,0,0,0,0,0,0,0,0,0,20,2,24,2,20,0,0,0,0,0,0,0,0,0,0,0,2,53,56,53,2,0,0,0,0,0,0,24,24,24,24,24,24,56,0,56,24,24,24,24,24,24,0,0,0,0,0,0,2,53,56,53,2,0,0,0,0,0,0,0,0,0,0,0,20,2,24,2,20,0,0,0,0,0,0,0,0,0,0,20,0,0,24,0,0,20,0,0,0,0,0,0,0,0,20,0,0,0,24,0,0,0,20,0,0,0,0,0,0,20,0,0,0,0,24,0,0,0,0,20,0,0,0,0,20,0,0,0,0,0,24,0,0,0,0,0,20,0,0,20,0,0,0,0,0,0,24,0,0,0,0,0,0,20],v=[17,0,0,0,0,0,0,16,0,0,0,0,0,0,15,0,0,17,0,0,0,0,0,16,0,0,0,0,0,15,0,0,0,0,17,0,0,0,0,16,0,0,0,0,15,0,0,0,0,0,0,17,0,0,0,16,0,0,0,15,0,0,0,0,0,0,0,0,17,0,0,16,0,0,15,0,0,0,0,0,0,0,0,0,0,17,0,16,0,15,0,0,0,0,0,0,0,0,0,0,0,0,17,16,15,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,-1,-1,-1,-1,-1,-1,-1,0,0,0,0,0,0,0,-15,-16,-17,0,0,0,0,0,0,0,0,0,0,0,0,-15,0,-16,0,-17,0,0,0,0,0,0,0,0,0,0,-15,0,0,-16,0,0,-17,0,0,0,0,0,0,0,0,-15,0,0,0,-16,0,0,0,-17,0,0,0,0,0,0,-15,0,0,0,0,-16,0,0,0,0,-17,0,0,0,0,-15,0,0,0,0,0,-16,0,0,0,0,0,-17,0,0,-15,0,0,0,0,0,0,-16,0,0,0,0,0,0,-17],h={p:0,n:1,b:2,r:3,q:4,k:5},o={NORMAL:"n",CAPTURE:"c",BIG_PAWN:"b",EP_CAPTURE:"e",PROMOTION:"p",KSIDE_CASTLE:"k",QSIDE_CASTLE:"q"},I={NORMAL:1,CAPTURE:2,BIG_PAWN:4,EP_CAPTURE:8,PROMOTION:16,KSIDE_CASTLE:32,QSIDE_CASTLE:64},P=7,w=6,L=1,R=0,N={a8:0,b8:1,c8:2,d8:3,e8:4,f8:5,g8:6,h8:7,a7:16,b7:17,c7:18,d7:19,e7:20,f7:21,g7:22,h7:23,a6:32,b6:33,c6:34,d6:35,e6:36,f6:37,g6:38,h6:39,a5:48,b5:49,c5:50,d5:51,e5:52,f5:53,g5:54,h5:55,a4:64,b4:65,c4:66,d4:67,e4:68,f4:69,g4:70,h4:71,a3:80,b3:81,c3:82,d3:83,e3:84,f3:85,g3:86,h3:87,a2:96,b2:97,c2:98,d2:99,e2:100,f2:101,g2:102,h2:103,a1:112,b1:113,c1:114,d1:115,e1:116,f1:117,g1:118,h1:119},E={w:[{square:N.a1,flag:I.QSIDE_CASTLE},{square:N.h1,flag:I.KSIDE_CASTLE}],b:[{square:N.a8,flag:I.QSIDE_CASTLE},{square:N.h8,flag:I.KSIDE_CASTLE}]},O=new Array(128),k={w:l,b:l},q=s,D={w:0,b:0},K=l,d=0,b=1,Q=[],U={};function x(r){void 0===r&&(r=!1),O=new Array(128),k={w:l,b:l},q=s,D={w:0,b:0},K=l,d=0,b=1,Q=[],r||(U={}),F(M())}function j(){B(e)}function B(r,e){void 0===e&&(e=!1);var n=r.split(/\s+/),t=n[0],o=0;if(!$(r).valid)return!1;x(e);for(var i=0;i<t.length;i++){var f=t.charAt(i);if("/"===f)o+=8;else if(-1!=="0123456789".indexOf(f))o+=parseInt(f,10);else{var a=f<"a"?s:u;W({type:f.toLowerCase(),color:a},fr(o)),o++}}return q=n[1],-1<n[2].indexOf("K")&&(D.w|=I.KSIDE_CASTLE),-1<n[2].indexOf("Q")&&(D.w|=I.QSIDE_CASTLE),-1<n[2].indexOf("k")&&(D.b|=I.KSIDE_CASTLE),-1<n[2].indexOf("q")&&(D.b|=I.QSIDE_CASTLE),K="-"===n[3]?l:N[n[3]],d=parseInt(n[4],10),b=parseInt(n[5],10),F(M()),!0}function $(r){var e="No errors.",n="FEN string must contain six space-delimited fields.",t="6th field (move number) must be a positive integer.",o="5th field (half move counter) must be a non-negative integer.",i="4th field (en-passant square) is invalid.",f="3rd field (castling availability) is invalid.",a="2nd field (side to move) is invalid.",l="1st field (piece positions) does not contain 8 '/'-delimited rows.",u="1st field (piece positions) is invalid [consecutive numbers].",s="1st field (piece positions) is invalid [invalid piece].",p="1st field (piece positions) is invalid [row too large].",c="Illegal en-passant square",v=r.split(/\s+/);if(6!==v.length)return{valid:!1,error_number:1,error:n};if(isNaN(v[5])||parseInt(v[5],10)<=0)return{valid:!1,error_number:2,error:t};if(isNaN(v[4])||parseInt(v[4],10)<0)return{valid:!1,error_number:3,error:o};if(!/^(-|[abcdefgh][36])$/.test(v[3]))return{valid:!1,error_number:4,error:i};if(!/^(KQ?k?q?|Qk?q?|kq?|q|-)$/.test(v[2]))return{valid:!1,error_number:5,error:f};if(!/^(w|b)$/.test(v[1]))return{valid:!1,error_number:6,error:a};var g=v[0].split("/");if(8!==g.length)return{valid:!1,error_number:7,error:l};for(var h=0;h<g.length;h++){for(var E=0,d=!1,b=0;b<g[h].length;b++)if(isNaN(g[h][b])){if(!/^[prnbqkPRNBQK]$/.test(g[h][b]))return{valid:!1,error_number:9,error:s};E+=1,d=!1}else{if(d)return{valid:!1,error_number:8,error:u};E+=parseInt(g[h][b],10),d=!0}if(8!==E)return{valid:!1,error_number:10,error:p}}return"3"==v[3][1]&&"w"==v[1]||"6"==v[3][1]&&"b"==v[1]?{valid:!1,error_number:11,error:c}:{valid:!0,error_number:0,error:e}}function M(){for(var r=0,e="",n=N.a8;n<=N.h1;n++){if(null==O[n])r++;else{0<r&&(e+=r,r=0);var t=O[n].color,o=O[n].type;e+=t===s?o.toUpperCase():o.toLowerCase()}n+1&136&&(0<r&&(e+=r),n!==N.h1&&(e+="/"),r=0,n+=8)}var i="";D[s]&I.KSIDE_CASTLE&&(i+="K"),D[s]&I.QSIDE_CASTLE&&(i+="Q"),D[u]&I.KSIDE_CASTLE&&(i+="k"),D[u]&I.QSIDE_CASTLE&&(i+="q"),i=i||"-";var f=K===l?"-":fr(K);return[e,q,i,f,d,b].join(" ")}function G(r){for(var e=0;e<r.length;e+=2)"string"==typeof r[e]&&"string"==typeof r[e+1]&&(U[r[e]]=r[e+1]);return U}function F(r){0<Q.length||(r!==e?(U.SetUp="1",U.FEN=r):(delete U.SetUp,delete U.FEN))}function i(r){var e=O[N[r]];return e?{type:e.type,color:e.color}:null}function W(r,e){if(!("type"in r&&"color"in r))return!1;if(-1===t.indexOf(r.type.toLowerCase()))return!1;if(!(e in N))return!1;var n=N[e];return(r.type!=p||k[r.color]==l||k[r.color]==n)&&(O[n]={type:r.type,color:r.color},r.type===p&&(k[r.color]=n),F(M()),!0)}function H(r,e,n,t,o){var i={color:q,from:e,to:n,flags:t,piece:r[e].type};return o&&(i.flags|=I.PROMOTION,i.promotion=o),r[n]?i.captured=r[n].type:t&I.EP_CAPTURE&&(i.captured=_),i}function Z(r){function e(r,e,n,t,o){if(r[n].type!==_||or(t)!==R&&or(t)!==P)e.push(H(r,n,t,o));else for(var i=[y,m,S,A],f=0,a=i.length;f<a;f++)e.push(H(r,n,t,o,i[f]))}var n=[],t=q,o=ar(t),i={b:L,w:w},f=N.a8,a=N.h1,l=!1,u=!(void 0!==r&&"legal"in r)||r.legal;if(void 0!==r&&"square"in r){if(!(r.square in N))return[];f=a=N[r.square],l=!0}for(var s=f;s<=a;s++)if(136&s)s+=7;else{var p=O[s];if(null!=p&&p.color===t)if(p.type===_){var c=s+C[t][0];if(null==O[c]){e(O,n,s,c,I.NORMAL);c=s+C[t][1];i[t]===or(s)&&null==O[c]&&e(O,n,s,c,I.BIG_PAWN)}for(v=2;v<4;v++){136&(c=s+C[t][v])||(null!=O[c]&&O[c].color===o?e(O,n,s,c,I.CAPTURE):c===K&&e(O,n,s,K,I.EP_CAPTURE))}}else for(var v=0,g=T[p.type].length;v<g;v++){var h=T[p.type][v];for(c=s;!(136&(c+=h));){if(null!=O[c]){if(O[c].color===t)break;e(O,n,s,c,I.CAPTURE);break}if(e(O,n,s,c,I.NORMAL),"n"===p.type||"k"===p.type)break}}}if(!l||a===k[t]){if(D[t]&I.KSIDE_CASTLE){var E=(d=k[t])+2;null!=O[d+1]||null!=O[E]||V(o,k[t])||V(o,d+1)||V(o,E)||e(O,n,k[t],E,I.KSIDE_CASTLE)}if(D[t]&I.QSIDE_CASTLE){var d;E=(d=k[t])-2;null!=O[d-1]||null!=O[d-2]||null!=O[d-3]||V(o,k[t])||V(o,d-1)||V(o,E)||e(O,n,k[t],E,I.QSIDE_CASTLE)}}if(!u)return n;var b=[];for(s=0,g=n.length;s<g;s++)er(n[s]),X(t)||b.push(n[s]),nr();return b}function z(r,e){var n="";if(r.flags&I.KSIDE_CASTLE)n="O-O";else if(r.flags&I.QSIDE_CASTLE)n="O-O-O";else{var t=function(r,e){for(var n=Z({legal:!e}),t=r.from,o=r.to,i=r.piece,f=0,a=0,l=0,u=0,s=n.length;u<s;u++){var p=n[u].from,c=n[u].to,v=n[u].piece;i===v&&t!==p&&o===c&&(f++,or(t)===or(p)&&a++,ir(t)===ir(p)&&l++)}if(0<f)return 0<a&&0<l?fr(t):0<l?fr(t).charAt(1):fr(t).charAt(0);return""}(r,e);r.piece!==_&&(n+=r.piece.toUpperCase()+t),r.flags&(I.CAPTURE|I.EP_CAPTURE)&&(r.piece===_&&(n+=fr(r.from)[0]),n+="x"),n+=fr(r.to),r.flags&I.PROMOTION&&(n+="="+r.promotion.toUpperCase())}return er(r),f()&&(a()?n+="#":n+="+"),nr(),n}function J(r){return r.replace(/=/,"").replace(/[+#]?[?!]*$/,"")}function V(r,e){for(var n=N.a8;n<=N.h1;n++)if(136&n)n+=7;else if(null!=O[n]&&O[n].color===r){var t=O[n],o=n-e,i=119+o;if(c[i]&1<<h[t.type]){if(t.type===_){if(0<o){if(t.color===s)return!0}else if(t.color===u)return!0;continue}if("n"===t.type||"k"===t.type)return!0;for(var f=v[i],a=n+f,l=!1;a!==e;){if(null!=O[a]){l=!0;break}a+=f}if(!l)return!0}}return!1}function X(r){return V(ar(r),k[r])}function f(){return X(q)}function a(){return f()&&0===Z().length}function n(){return!f()&&0===Z().length}function Y(){for(var r={},e=[],n=0,t=0,o=N.a8;o<=N.h1;o++)if(t=(t+1)%2,136&o)o+=7;else{var i=O[o];i&&(r[i.type]=i.type in r?r[i.type]+1:1,i.type===S&&e.push(t),n++)}if(2===n)return!0;if(3===n&&(1===r[S]||1===r[A]))return!0;if(n===r[S]+2){var f=0,a=e.length;for(o=0;o<a;o++)f+=e[o];if(0===f||f===a)return!0}return!1}function rr(){for(var r=[],e={},n=!1;;){var t=nr();if(!t)break;r.push(t)}for(;;){var o=M().split(" ").slice(0,4).join(" ");if(e[o]=o in e?e[o]+1:1,3<=e[o]&&(n=!0),!r.length)break;er(r.pop())}return n}function er(r){var e,n=q,t=ar(n);if(e=r,Q.push({move:e,kings:{b:k.b,w:k.w},turn:q,castling:{b:D.b,w:D.w},ep_square:K,half_moves:d,move_number:b}),O[r.to]=O[r.from],O[r.from]=null,r.flags&I.EP_CAPTURE&&(q===u?O[r.to-16]=null:O[r.to+16]=null),r.flags&I.PROMOTION&&(O[r.to]={type:r.promotion,color:n}),O[r.to].type===p){if(k[O[r.to].color]=r.to,r.flags&I.KSIDE_CASTLE){var o=r.to-1,i=r.to+1;O[o]=O[i],O[i]=null}else if(r.flags&I.QSIDE_CASTLE){o=r.to+1,i=r.to-2;O[o]=O[i],O[i]=null}D[n]=""}if(D[n])for(var f=0,a=E[n].length;f<a;f++)if(r.from===E[n][f].square&&D[n]&E[n][f].flag){D[n]^=E[n][f].flag;break}if(D[t])for(f=0,a=E[t].length;f<a;f++)if(r.to===E[t][f].square&&D[t]&E[t][f].flag){D[t]^=E[t][f].flag;break}K=r.flags&I.BIG_PAWN?"b"===q?r.to-16:r.to+16:l,r.piece===_||r.flags&(I.CAPTURE|I.EP_CAPTURE)?d=0:d++,q===u&&b++,q=ar(q)}function nr(){var r=Q.pop();if(null==r)return null;var e=r.move;k=r.kings,q=r.turn,D=r.castling,K=r.ep_square,d=r.half_moves,b=r.move_number;var n,t,o=q,i=ar(q);if(O[e.from]=O[e.to],O[e.from].type=e.piece,O[e.to]=null,e.flags&I.CAPTURE)O[e.to]={type:e.captured,color:i};else if(e.flags&I.EP_CAPTURE){var f;f=o===u?e.to-16:e.to+16,O[f]={type:_,color:i}}e.flags&(I.KSIDE_CASTLE|I.QSIDE_CASTLE)&&(e.flags&I.KSIDE_CASTLE?(n=e.to+1,t=e.to-1):e.flags&I.QSIDE_CASTLE&&(n=e.to-2,t=e.to+1),O[n]=O[t],O[t]=null);return e}function tr(r,e){var n=J(r);if(e){var t=n.match(/([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])?/);if(t)var o=t[1],i=t[2],f=t[3],a=t[4]}for(var l=Z(),u=0,s=l.length;u<s;u++){if(n===J(z(l[u]))||e&&n===J(z(l[u],!0)))return l[u];if(t&&(!o||o.toLowerCase()==l[u].piece)&&N[i]==l[u].from&&N[f]==l[u].to&&(!a||a.toLowerCase()==l[u].promotion))return l[u]}return null}function or(r){return r>>4}function ir(r){return 15&r}function fr(r){var e=ir(r),n=or(r);return"abcdefgh".substring(e,e+1)+"87654321".substring(n,n+1)}function ar(r){return r===s?u:s}function lr(r){var e=function r(e){var n=e instanceof Array?[]:{};for(var t in e)n[t]="object"==typeof t?r(e[t]):e[t];return n}(r);e.san=z(e,!1),e.to=fr(e.to),e.from=fr(e.from);var n="";for(var t in I)I[t]&e.flags&&(n+=o[t]);return e.flags=n,e}function ur(r){return r.replace(/^\s+|\s+$/g,"")}return B(void 0===r?e:r),{WHITE:s,BLACK:u,PAWN:_,KNIGHT:A,BISHOP:S,ROOK:m,QUEEN:y,KING:p,SQUARES:function(){for(var r=[],e=N.a8;e<=N.h1;e++)136&e?e+=7:r.push(fr(e));return r}(),FLAGS:o,load:function(r){return B(r)},reset:function(){return j()},moves:function(r){for(var e=Z(r),n=[],t=0,o=e.length;t<o;t++)void 0!==r&&"verbose"in r&&r.verbose?n.push(lr(e[t])):n.push(z(e[t],!1));return n},in_check:function(){return f()},in_checkmate:function(){return a()},in_stalemate:function(){return n()},in_draw:function(){return 100<=d||n()||Y()||rr()},insufficient_material:function(){return Y()},in_threefold_repetition:function(){return rr()},game_over:function(){return 100<=d||a()||n()||Y()||rr()},validate_fen:function(r){return $(r)},fen:function(){return M()},board:function(){for(var r=[],e=[],n=N.a8;n<=N.h1;n++)null==O[n]?e.push(null):e.push({type:O[n].type,color:O[n].color}),n+1&136&&(r.push(e),e=[],n+=8);return r},pgn:function(r){var e="object"==typeof r&&"string"==typeof r.newline_char?r.newline_char:"\n",n="object"==typeof r&&"number"==typeof r.max_width?r.max_width:0,t=[],o=!1;for(var i in U)t.push("["+i+' "'+U[i]+'"]'+e),o=!0;o&&Q.length&&t.push(e);for(var f=[];0<Q.length;)f.push(nr());for(var a=[],l="";0<f.length;){var u=f.pop();Q.length||"b"!==u.color?"w"===u.color&&(l.length&&a.push(l),l=b+"."):l=b+". ...",l=l+" "+z(u,!1),er(u)}if(l.length&&a.push(l),void 0!==U.Result&&a.push(U.Result),0===n)return t.join("")+a.join(" ");var s=0;for(i=0;i<a.length;i++)s+a[i].length>n&&0!==i?(" "===t[t.length-1]&&t.pop(),t.push(e),s=0):0!==i&&(t.push(" "),s++),t.push(a[i]),s+=a[i].length;return t.join("")},load_pgn:function(r,e){var n=void 0!==e&&"sloppy"in e&&e.sloppy;function l(r){return r.replace(/\\/g,"\\")}var t="object"==typeof e&&"string"==typeof e.newline_char?e.newline_char:"\r?\n",o=new RegExp("^(\\[((?:"+l(t)+")|.)*\\])(?:"+l(t)+"){2}"),i=o.test(r)?o.exec(r)[1]:"";j();var f=function(r,e){for(var n="object"==typeof e&&"string"==typeof e.newline_char?e.newline_char:"\r?\n",t={},o=r.split(new RegExp(l(n))),i="",f="",a=0;a<o.length;a++)i=o[a].replace(/^\[([A-Z][A-Za-z]*)\s.*\]$/,"$1"),f=o[a].replace(/^\[[A-Za-z]+\s"(.*)"\]$/,"$1"),0<ur(i).length&&(t[i]=f);return t}(i,e);for(var a in f)G([a,f[a]]);if("1"===f.SetUp&&!("FEN"in f&&B(f.FEN,!0)))return!1;var u=r.replace(i,"").replace(new RegExp(l(t),"g")," ");u=u.replace(/(\{[^}]+\})+?/g,"");for(var s=/(\([^\(\)]+\))+?/g;s.test(u);)u=u.replace(s,"");var p=ur(u=(u=(u=u.replace(/\d+\.(\.\.)?/g,"")).replace(/\.\.\./g,"")).replace(/\$\d+/g,"")).split(new RegExp(/\s+/));p=p.join(",").replace(/,,+/g,",").split(",");for(var c="",v=0;v<p.length-1;v++){if(null==(c=tr(p[v],n)))return!1;er(c)}if(c=p[p.length-1],-1<g.indexOf(c))!function(r){for(var e in r)return 1}(U)||void 0!==U.Result||G(["Result",c]);else{if(null==(c=tr(c,n)))return!1;er(c)}return!0},header:function(){return G(arguments)},ascii:function(){return function(){for(var r=" +------------------------+\n",e=N.a8;e<=N.h1;e++){if(0===ir(e)&&(r+=" "+"87654321"[or(e)]+" |"),null==O[e])r+=" . ";else{var n=O[e].type;r+=" "+(O[e].color===s?n.toUpperCase():n.toLowerCase())+" "}e+1&136&&(r+="|\n",e+=8)}return r+=" +------------------------+\n",r+=" a b c d e f g h\n"}()},turn:function(){return q},move:function(r,e){var n=void 0!==e&&"sloppy"in e&&e.sloppy,t=null;if("string"==typeof r)t=tr(r,n);else if("object"==typeof r)for(var o=Z(),i=0,f=o.length;i<f;i++)if(!(r.from!==fr(o[i].from)||r.to!==fr(o[i].to)||"promotion"in o[i]&&r.promotion!==o[i].promotion)){t=o[i];break}if(!t)return null;var a=lr(t);return er(t),a},undo:function(){var r=nr();return r?lr(r):null},clear:function(){return x()},put:function(r,e){return W(r,e)},get:function(r){return i(r)},remove:function(r){return n=i(e=r),O[N[e]]=null,n&&n.type===p&&(k[n.color]=l),F(M()),n;var e,n},perft:function(r){return function r(e){for(var n=Z({legal:!1}),t=0,o=q,i=0,f=n.length;i<f;i++)er(n[i]),X(o)||(0<e-1?t+=r(e-1):t++),nr();return t}(r)},square_color:function(r){if(r in N){var e=N[r];return(or(e)+ir(e))%2==0?"light":"dark"}return null},history:function(r){for(var e=[],n=[],t=(void 0!==r&&"verbose"in r&&r.verbose);0<Q.length;)e.push(nr());for(;0<e.length;){var o=e.pop();t?n.push(lr(o)):n.push(z(o)),er(o)}return n}}};"undefined"!=typeof exports&&(exports.Chess=Chess),"undefined"!=typeof define&&define(function(){return Chess});
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment