From a6d0c5901e40030f3a2273cacfd28aa953e52b54 Mon Sep 17 00:00:00 2001 From: nullprop Date: Thu, 21 Sep 2023 19:00:24 +0300 Subject: [PATCH] Final submission --- Chess-Challenge/src/Evil Bot/EvilBot.cs | 117 ++++++++---- Chess-Challenge/src/My Bot/MyBot.cs | 238 +++++++++++++++++++++++- 2 files changed, 315 insertions(+), 40 deletions(-) diff --git a/Chess-Challenge/src/Evil Bot/EvilBot.cs b/Chess-Challenge/src/Evil Bot/EvilBot.cs index b70d8e1..d66cbed 100644 --- a/Chess-Challenge/src/Evil Bot/EvilBot.cs +++ b/Chess-Challenge/src/Evil Bot/EvilBot.cs @@ -1,54 +1,99 @@ -using ChessChallenge.API; -using System; +/* + * Stockfish UCI implementation + * for evaluating changes to MyBot. + */ namespace ChessChallenge.Example { - // A simple bot that can spot mate in one, and always captures the most valuable piece it can. - // Plays randomly otherwise. + using System; + using System.Diagnostics; + using ChessChallenge.API; + public class EvilBot : IChessBot { - // Piece values: null, pawn, knight, bishop, rook, queen, king - int[] pieceValues = { 0, 100, 300, 300, 500, 900, 10000 }; + Process stockfish; + + public EvilBot() + { + stockfish = new(); + stockfish.StartInfo.RedirectStandardInput = true; + stockfish.StartInfo.RedirectStandardOutput = true; + stockfish.StartInfo.FileName = "/bin/stockfish"; + stockfish.Start(); + + WriteLine("uci"); + if (!IsOk()) + { + throw new Exception("Stockfish is not ok"); + } + + // WriteLine($"setoption name Skill Level value {3}"); + WriteLine("setoption name UCI_LimitStrength value true"); + WriteLine("setoption name UCI_ELO value 1650"); // min 1320 max 3190 + WriteLine("setoption name Threads value 16"); + WriteLine("setoption name Ponder value false"); + WriteLine("ucinewgame"); + } public Move Think(Board board, Timer timer) { - Move[] allMoves = board.GetLegalMoves(); + WriteLine($"position fen {board.GetFenString()}"); - // Pick a random move to play if nothing better is found - Random rng = new(); - Move moveToPlay = allMoves[rng.Next(allMoves.Length)]; - int highestValueCapture = 0; + var ourTeam = board.IsWhiteToMove ? "w" : "b"; + var enemyTeam = board.IsWhiteToMove ? "b" : "w"; + WriteLine($"go {ourTeam}time {timer.MillisecondsRemaining} {enemyTeam}time {timer.OpponentMillisecondsRemaining}"); - foreach (Move move in allMoves) + Move? move = GetBestMove(board); + if (move == null) { - // Always play checkmate in one - if (MoveIsCheckmate(board, move)) - { - moveToPlay = move; - break; - } - - // Find highest value capture - Piece capturedPiece = board.GetPiece(move.TargetSquare); - int capturedPieceValue = pieceValues[(int)capturedPiece.PieceType]; - - if (capturedPieceValue > highestValueCapture) - { - moveToPlay = move; - highestValueCapture = capturedPieceValue; - } + throw new Exception("Stockfish returned no move"); } - - return moveToPlay; + return (Move)move; } - // Test if this move gives checkmate - bool MoveIsCheckmate(Board board, Move move) + string? ReadLine() { - board.MakeMove(move); - bool isMate = board.IsInCheckmate(); - board.UndoMove(move); - return isMate; + var line = stockfish.StandardOutput.ReadLine(); + // if (line != null) + // { + // Console.WriteLine($"stockfish: {line}"); + // } + return line; + } + + void WriteLine(string line) + { + stockfish.StandardInput.WriteLine(line); + } + + private bool IsOk() + { + string? line; + var ok = false; + while ((line = ReadLine()) != null) + { + if (line == "uciok") + { + ok = true; + break; + } + } + return ok; + } + + private Move? GetBestMove(Board board) + { + string? line; + Move? move = null; + while ((line = ReadLine()) != null) + { + if (line.StartsWith("bestmove")) + { + move = new Move(line.Split()[1], board); + break; + } + } + return move; } } } \ No newline at end of file diff --git a/Chess-Challenge/src/My Bot/MyBot.cs b/Chess-Challenge/src/My Bot/MyBot.cs index 3c2a704..a2e1382 100644 --- a/Chess-Challenge/src/My Bot/MyBot.cs +++ b/Chess-Challenge/src/My Bot/MyBot.cs @@ -1,10 +1,240 @@ -using ChessChallenge.API; +/* + * Sebastian Lague's Tiny Chess Challenge + * https://github.com/SebLague/Chess-Challenge + * https://youtu.be/iScy18pVR58 + * + * Submission by nullprop + * https://nullprop.sh/ + * + * PVS with hand written eval. + * Somewhat sketchy adaptive depth, + * based on previous think time and EBF in alpha-beta. + * Roughly 1650 elo according to Stockfish UCI_ELO. + */ + +using System; +using System.Linq; +using ChessChallenge.API; public class MyBot : IChessBot { + int[] pieceValues; + int lastThinkTime; + int maxDepth = 5; + int lastEval; + Move[] moveList; + int nodesThisTurn;// #DEBUG + public Move Think(Board board, Timer timer) { - Move[] moves = board.GetLegalMoves(); - return moves[0]; + if (lastThinkTime > 0) + { + if (lastThinkTime > timer.MillisecondsRemaining * 0.1f) + maxDepth--; + else + { + // b ^ ceil(n/2) + b ^ floor(n/2) when b = 40 => 1.95, 20.5 alternating + var timeEstimate = lastThinkTime * (maxDepth % 2 == 0 ? 20.5f : 1.95f); + if (timeEstimate < timer.MillisecondsRemaining * 0.05f && lastEval < 999) + maxDepth++; + } + // going below this is no bueno, fast anyways. + // probably shouldn't happen unless running on a potato. + if (maxDepth < 2) maxDepth = 2; + } + + // Change piece values based on how far we are in the game + var pieceValueMult = MathF.Min(1.0f, board.PlyCount / 60.0f); + pieceValues = new int[]{ + 88 + (int)(pieceValueMult * 24), // pawn + 394 - (int)(pieceValueMult * 55), // knight + 428 - (int)(pieceValueMult * 71), // bishop + 572 + (int)(pieceValueMult * 42), // rook + 1240 - (int)(pieceValueMult * 116), // queen + 0 // king + }; + + moveList = new Move[maxDepth]; + var eval = Search(board, -10001, 10000, maxDepth); + + Console.WriteLine($"\n\n"); // #DEBUG + Console.WriteLine(board.GetFenString()); // #DEBUG + Console.WriteLine($"Depth: {maxDepth}"); // #DEBUG + Console.WriteLine($"Speed: {nodesThisTurn / (timer.MillisecondsElapsedThisTurn + 1)}k nodes/s"); // #DEBUG + Console.WriteLine($"Think: {timer.MillisecondsElapsedThisTurn}ms / {timer.MillisecondsRemaining}ms"); // #DEBUG + Console.WriteLine($"Eval: {eval / 100f}"); // #DEBUG + for (int i = maxDepth - 1; i >= 0; i--) // #DEBUG + Console.WriteLine($" {(i % 2 == 0 ? "" : "#")}{moveList[i]} ({moveList[i].MovePieceType})"); // #DEBUG + nodesThisTurn = 0; // #DEBUG + + lastThinkTime = timer.MillisecondsElapsedThisTurn + 5; // fudge + lastEval = eval; + return moveList[maxDepth - 1]; } -} \ No newline at end of file + + private int Search(Board board, int alpha, int beta, int depth) + { + nodesThisTurn++; // #DEBUG + + if (depth == 0) + { + // quiesce + int eval = EvalBoard(board); + + if (eval >= beta) return beta; + if (alpha < eval) alpha = eval; + + foreach (var capture in board.GetLegalMoves(true)) + { + board.MakeMove(capture); + eval = -Search(board, -beta, -alpha, 0); + board.UndoMove(capture); + if (eval >= beta) return beta; + if (alpha < eval) alpha = eval; + } + + return alpha; + } + + var searchPv = true; + + foreach (var move in board.GetLegalMoves().OrderByDescending(m => (m.IsCapture ? 100 * (int)m.CapturePieceType - (int)m.MovePieceType : 0) + (m.IsPromotion ? 1 : 0))) + { + board.MakeMove(move); + + int score = 0; + if (searchPv) + { + score = -Search(board, -beta, -alpha, depth - 1); + moveList[depth - 1] = move; + } + else + { + score = -Search(board, -alpha - 1, -alpha, depth - 1); + if (score > alpha) + score = -Search(board, -beta, -alpha, depth - 1); + } + + board.UndoMove(move); + + if (score >= beta) + { + moveList[depth - 1] = move; + return beta; + } + if (score > alpha) + { + moveList[depth - 1] = move; + alpha = score; + searchPv = false; + } + } + + return alpha; + } + + private int EvalBoard(Board board) + { + if (board.IsInCheckmate()) + return -10000; + + if (board.IsDraw()) + return -50; // contempt + + // token optimization + var whiteToMove = board.IsWhiteToMove ? 1 : -1; + var pieces = board.GetAllPieceLists(); + var openingWeight = 3.0f / board.PlyCount; + + var GetPieceBitboard = board.GetPieceBitboard; + var GetNumberOfSetBits = BitboardHelper.GetNumberOfSetBits; + + var whitePawnBB = GetPieceBitboard(PieceType.Pawn, true); + var blackPawnBB = GetPieceBitboard(PieceType.Pawn, false); + var whiteKnightBB = GetPieceBitboard(PieceType.Knight, true); + var blackKnightBB = GetPieceBitboard(PieceType.Knight, false); + var whiteBishopBB = GetPieceBitboard(PieceType.Bishop, true); + var blackBishopBB = GetPieceBitboard(PieceType.Bishop, false); + var whiteQueenBB = GetPieceBitboard(PieceType.Queen, true); + var blackQueenBB = GetPieceBitboard(PieceType.Queen, false); + var whitePiecesBB = board.WhitePiecesBitboard; + var blackPiecesBB = board.BlackPiecesBitboard; + + // mobility + // GetLegalMoves is very slow here. + // Will never be cached since we call MakeMove right before eval. + // Estimate legal moves by checked state and number of pieces. + int eval = ( + (board.IsInCheck() ? 2 : GetNumberOfSetBits(board.IsWhiteToMove ? whitePiecesBB : blackPiecesBB)) + ) * 25 * whiteToMove; + + // material + for (int i = 0; i < 5; i++) + eval += (pieces[i].Count - pieces[i + 6].Count) * pieceValues[i]; + + // pawns + ulong pawnFiles = 0; + var wpCount = pieces[0].Count; + for (int i = 0; i < wpCount; i++) + { + var pawn = pieces[0][i]; + // advance & structure + eval += 10 * (2 * pawn.Square.Rank + GetNumberOfSetBits(whitePawnBB & BitboardHelper.GetPawnAttacks(pawn.Square, true))); + // doubling + pawnFiles |= 1U << pawn.Square.File; + } + eval -= wpCount - GetNumberOfSetBits(pawnFiles); + pawnFiles = 0; + var bpCount = pieces[6].Count; + for (int i = 0; i < bpCount; i++) + { + var pawn = pieces[6][i]; + eval -= 10 * (2 * (7 - pawn.Square.Rank) + GetNumberOfSetBits(blackPawnBB & BitboardHelper.GetPawnAttacks(pawn.Square, false))); + pawnFiles |= 1U << pawn.Square.File; + } + eval += bpCount - GetNumberOfSetBits(pawnFiles); + + // center occupancy + // extended 4x4 center + eval += (20 + (int)(openingWeight * 50)) * ( + GetNumberOfSetBits(whitePawnBB & 0x00003C3C3C3C0000) - + GetNumberOfSetBits(blackPawnBB & 0x00003C3C3C3C0000) + ); + // 2x2 center + eval += (10 + (int)(openingWeight * 50)) * ( + GetNumberOfSetBits(whitePawnBB & 0x0000001818000000) - + GetNumberOfSetBits(blackPawnBB & 0x0000001818000000) + ); + + // king protecting pawns / king pawn shield + eval += 20 * ( + GetNumberOfSetBits(whitePawnBB & BitboardHelper.GetKingAttacks(board.GetKingSquare(true))) - + GetNumberOfSetBits(blackPawnBB & BitboardHelper.GetKingAttacks(board.GetKingSquare(false))) + ); + + // development + eval += (20 + (int)(openingWeight * 4)) * ( + GetNumberOfSetBits((whiteKnightBB | whiteBishopBB | whiteQueenBB) & 0xFFFFFFFFFFFF00) - + GetNumberOfSetBits((blackKnightBB | blackBishopBB | blackQueenBB) & 0x00FFFFFFFFFFFF) + ); + + // edges are generally bad for these pieces + eval += 30 * ( + GetNumberOfSetBits((blackKnightBB | blackBishopBB | blackQueenBB) & 0xFF818181818181FF) - + GetNumberOfSetBits((whiteKnightBB | whiteBishopBB | whiteQueenBB) & 0xFF818181818181FF) + ); + + // Rooks are generally good on low ranks + eval += 5 * ( + GetNumberOfSetBits(GetPieceBitboard(PieceType.Rook, true) & 0x00000000FFFFFFFF) - + GetNumberOfSetBits(GetPieceBitboard(PieceType.Rook, false) & 0xFFFFFFFF00000000) + ); + + // Having both bishops is good + if (pieces[2].Count > 1) eval += 5; + if (pieces[8].Count > 1) eval -= 5; + + // signed for the side playing + return eval * whiteToMove; + } +}