Final submission
This commit is contained in:
parent
ec7cf32665
commit
a6d0c5901e
2 changed files with 315 additions and 40 deletions
|
@ -1,54 +1,99 @@
|
||||||
using ChessChallenge.API;
|
/*
|
||||||
using System;
|
* Stockfish UCI implementation
|
||||||
|
* for evaluating changes to MyBot.
|
||||||
|
*/
|
||||||
|
|
||||||
namespace ChessChallenge.Example
|
namespace ChessChallenge.Example
|
||||||
{
|
{
|
||||||
// A simple bot that can spot mate in one, and always captures the most valuable piece it can.
|
using System;
|
||||||
// Plays randomly otherwise.
|
using System.Diagnostics;
|
||||||
|
using ChessChallenge.API;
|
||||||
|
|
||||||
public class EvilBot : IChessBot
|
public class EvilBot : IChessBot
|
||||||
{
|
{
|
||||||
// Piece values: null, pawn, knight, bishop, rook, queen, king
|
Process stockfish;
|
||||||
int[] pieceValues = { 0, 100, 300, 300, 500, 900, 10000 };
|
|
||||||
|
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)
|
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
|
var ourTeam = board.IsWhiteToMove ? "w" : "b";
|
||||||
Random rng = new();
|
var enemyTeam = board.IsWhiteToMove ? "b" : "w";
|
||||||
Move moveToPlay = allMoves[rng.Next(allMoves.Length)];
|
WriteLine($"go {ourTeam}time {timer.MillisecondsRemaining} {enemyTeam}time {timer.OpponentMillisecondsRemaining}");
|
||||||
int highestValueCapture = 0;
|
|
||||||
|
|
||||||
foreach (Move move in allMoves)
|
Move? move = GetBestMove(board);
|
||||||
|
if (move == null)
|
||||||
{
|
{
|
||||||
// Always play checkmate in one
|
throw new Exception("Stockfish returned no move");
|
||||||
if (MoveIsCheckmate(board, move))
|
}
|
||||||
|
return (Move)move;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? ReadLine()
|
||||||
{
|
{
|
||||||
moveToPlay = move;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
// Find highest value capture
|
private Move? GetBestMove(Board board)
|
||||||
Piece capturedPiece = board.GetPiece(move.TargetSquare);
|
|
||||||
int capturedPieceValue = pieceValues[(int)capturedPiece.PieceType];
|
|
||||||
|
|
||||||
if (capturedPieceValue > highestValueCapture)
|
|
||||||
{
|
{
|
||||||
moveToPlay = move;
|
string? line;
|
||||||
highestValueCapture = capturedPieceValue;
|
Move? move = null;
|
||||||
}
|
while ((line = ReadLine()) != null)
|
||||||
}
|
|
||||||
|
|
||||||
return moveToPlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test if this move gives checkmate
|
|
||||||
bool MoveIsCheckmate(Board board, Move move)
|
|
||||||
{
|
{
|
||||||
board.MakeMove(move);
|
if (line.StartsWith("bestmove"))
|
||||||
bool isMate = board.IsInCheckmate();
|
{
|
||||||
board.UndoMove(move);
|
move = new Move(line.Split()[1], board);
|
||||||
return isMate;
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return move;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
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)
|
public Move Think(Board board, Timer timer)
|
||||||
{
|
{
|
||||||
Move[] moves = board.GetLegalMoves();
|
if (lastThinkTime > 0)
|
||||||
return moves[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];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue