From 90528e719156cc3e02f7b5d0f3640947c64014d1 Mon Sep 17 00:00:00 2001 From: Christian Dean Date: Sun, 15 Aug 2021 20:49:52 -0500 Subject: [PATCH] Inital commit of Blunder 5.0.0 rewrite --- README.md | 51 +- ai/evaluation.go | 266 ------ ai/search.go | 284 ------ ai/time.go | 36 - blunder/main.go | 67 +- engine/bitboard.go | 113 +-- engine/board.go | 838 ++++++++++-------- engine/evaluation.go | 228 +++++ engine/move.go | 183 ++-- engine/movegen.go | 322 +++---- tests/perfttest.go => engine/movegen_test.go | 25 +- engine/movelist.go | 15 + engine/polyglot.go | 125 --- engine/search.go | 456 ++++++++++ engine/tables.go | 377 ++------ engine/timemanager.go | 63 ++ engine/transposition.go | 155 ++++ engine/uci.go | 185 ++++ engine/utils.go | 36 + engine/zobrist.go | 153 ++++ engine/magic.go => extra/magicgen.go | 61 +- .../perftsuite.txt => testdata/perftsuite.epd | 0 tests/polyglot_test_files/test1.bin | Bin 1344 -> 0 bytes tests/polyglot_test_files/test10.bin | Bin 1152 -> 0 bytes tests/polyglot_test_files/test11.bin | Bin 288 -> 0 bytes tests/polyglot_test_files/test2.bin | Bin 2032 -> 0 bytes tests/polyglot_test_files/test3.bin | Bin 1744 -> 0 bytes tests/polyglot_test_files/test4.bin | Bin 1216 -> 0 bytes tests/polyglot_test_files/test5.bin | Bin 944 -> 0 bytes tests/polyglot_test_files/test6.bin | Bin 1168 -> 0 bytes tests/polyglot_test_files/test7.bin | Bin 1920 -> 0 bytes tests/polyglot_test_files/test8.bin | Bin 2064 -> 0 bytes tests/polyglot_test_files/test9.bin | Bin 1648 -> 0 bytes tests/zobristtest.go | 103 --- ui/cmd.go | 100 --- ui/uci.go | 121 --- 36 files changed, 2173 insertions(+), 2190 deletions(-) delete mode 100644 ai/evaluation.go delete mode 100644 ai/search.go delete mode 100644 ai/time.go create mode 100644 engine/evaluation.go rename tests/perfttest.go => engine/movegen_test.go (87%) create mode 100644 engine/movelist.go delete mode 100644 engine/polyglot.go create mode 100644 engine/search.go create mode 100644 engine/timemanager.go create mode 100644 engine/transposition.go create mode 100644 engine/uci.go create mode 100644 engine/utils.go create mode 100644 engine/zobrist.go rename engine/magic.go => extra/magicgen.go (90%) rename tests/perftsuite.txt => testdata/perftsuite.epd (100%) delete mode 100644 tests/polyglot_test_files/test1.bin delete mode 100644 tests/polyglot_test_files/test10.bin delete mode 100644 tests/polyglot_test_files/test11.bin delete mode 100644 tests/polyglot_test_files/test2.bin delete mode 100644 tests/polyglot_test_files/test3.bin delete mode 100644 tests/polyglot_test_files/test4.bin delete mode 100644 tests/polyglot_test_files/test5.bin delete mode 100644 tests/polyglot_test_files/test6.bin delete mode 100644 tests/polyglot_test_files/test7.bin delete mode 100644 tests/polyglot_test_files/test8.bin delete mode 100644 tests/polyglot_test_files/test9.bin delete mode 100644 tests/zobristtest.go delete mode 100644 ui/cmd.go delete mode 100644 ui/uci.go diff --git a/README.md b/README.md index de12f5e..cbc5e74 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ Overview Blunder is an open-source UCI compatible chess engine. The philosophy behind Blunder's design is for the code to straightforward and easy to read, so that others can benefit from the project. Currently my estimate is that Blunder -is at about ~1830 Elo (in self-play). +is at about 1900-2000 Elo (in self-play), and Blunder 4.0.0 is rated [1700 on the CCRL Blitz list](http://ccrl.chessdom.com/ccrl/404/cgi/engine_details.cgi?print=Details&each_game=1&eng=Blunder%204.0.0%2064-bit#Blunder_4_0_0_64-bit). Blunder 5.0.0 has yet to +be tested. Installation ----- -Blunder is current being developed in a linux 64-bit enviorment, so that is the executable that is provided. However, -compiling Blunder on different machines is fairly simple. +Compiling Blunder is fairly simple. All that is needed is to visit [Golang downlaod page](https://golang.org/dl/), and install Golang using the download package appropriate for your machine. To make using the Golang compiler easier, make sure that if the installer asks, @@ -35,36 +35,6 @@ So to use Blunder, it's reccomend you install one of these programs. Popular fre Once you have a program downloaded, you'll need to follow that specfic programs guide on how to install a chess engine. When prompted for a command or executable, direct the GUI to the Golang exectuable you built. -In addition to being UCI compatible, Blunder has a simple command line interface. To use it, start the executable, and type in `debug`. -But wait 3-5 seconds before entering debug, as Blunder needs a second or two to initalize its internals. After typing in `debug`, you -should be greeted with an ASCII display of Blunder's current board state, and a prompt to enter a command: - -``` - -8 | r n b q k b n r -7 | p p p p p p p p -6 | . . . . . . . . -5 | . . . . . . . . -4 | . . . . . . . . -3 | . . . . . . . . -2 | P P P P P P P P -1 | R N B Q K B N R - ---------------- - a b c d e f g h - -turn: white -castling rights: KQkq -en passant: none -rule 50: 0 -game ply: 0 - ->> - -``` - -Type "options" into the prompt -to see the available commands. - Features -------- @@ -73,7 +43,6 @@ Features - [Magic bitboards for slider move generation](https://www.chessprogramming.org/Magic_Bitboards) - [Zobrist hashing](). - [Transposition table for perft](). - - Basic filtered move generation (not to be confused with STAGED move generation). * Search - [Negamax search framework](https://www.chessprogramming.org/Negamax) - [Alpha-Beta pruning](https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning) @@ -82,22 +51,12 @@ Features - [Time-control logic supporting classical, rapid, bullet, and ultra-bullet time formats](https://www.chessprogramming.org/Time_Management). - [Repition detection](https://www.chessprogramming.org/Repetitions). - [Killer moves](https://www.chessprogramming.org/Killer_Move). + - [Transposition table](https://www.chessprogramming.org/Transposition_Table). + - [Null-move pruning](https://www.chessprogramming.org/Null_Move_Pruning). * Evaluation - [Material evaluation](https://www.chessprogramming.org/Material) - [Tuned piece-square tables](https://www.chessprogramming.org/Piece-Square_Tables) - [Tapered evaluation](https://www.chessprogramming.org/Tapered_Eval). - - Future Features - --------------- - One of the most fun and exciting parts of chess programming is adding new features to your engine, and watching it - slowly become stronger and better than previous versions. - - With that said, here is a list of the next few features planned for blunder 4.0.0. Note that these are subject to change: - - * Transposition table - * Principal variation search - * King saftey evaluation - * Pawn structure evaluation Changelog --------- diff --git a/ai/evaluation.go b/ai/evaluation.go deleted file mode 100644 index 51dccf8..0000000 --- a/ai/evaluation.go +++ /dev/null @@ -1,266 +0,0 @@ -package ai - -import ( - "blunder/engine" -) - -const ( - PawnValue = 100 - KnightValue = 300 - BishopValue = 330 - RookValue = 500 - QueenValue = 900 - - PawnPhase = 0 - KnightPhase = 1 - BishopPhase = 1 - RookPhase = 2 - QueenPhase = 4 - TotalPhase = PawnPhase*16 + KnightPhase*4 + BishopPhase*4 + RookPhase*4 + QueenPhase*2 - - PosInf = 10000 - NegInf = -PosInf -) - -// Below are tables containg piece square tables for each piece, indexed -// by their bitboard index (see the constants in board.go) -// -// Credit to Maksym Korzh for the piece square table values, which can -// be found here: -// -// https://github.com/maksimKorzh/wukongJS/blob/main/tools/eval_tuner/temp_weights/session_weights_2021-01-08-18-52.txt - -var PSQT_MG [6][64]int = [6][64]int{ - - // Piece-square table for pawns - { - 0, 0, 0, 0, 0, 0, 0, 0, - 49, 49, 50, 50, 51, 49, 49, 49, - 11, 11, 19, 31, 29, 21, 10, 9, - 5, 4, 9, 25, 25, 11, 4, 6, - 1, 0, 1, 19, 19, -1, -1, -1, - 4, -6, -10, 0, 0, -10, -4, 4, - 5, 11, 11, -19, -19, 11, 11, 4, - 0, 0, 0, 0, 0, 0, 0, 0, - }, - - // Piece-square table for knights - { - -51, -40, -30, -29, -29, -30, -40, -50, - -40, -19, 0, -1, 0, -1, -20, -40, - -29, 1, 11, 14, 15, 9, 0, -29, - -30, 6, 16, 19, 19, 15, 6, -31, - -31, 0, 15, 19, 19, 16, -1, -29, - -29, 4, 9, 16, 14, 11, 6, -29, - -39, -19, 1, 4, 6, 0, -19, -39, - -49, -39, -30, -29, -31, -29, -39, -51, - }, - - // Piece-square table for bishops - { - -19, -9, -9, -9, -10, -9, -11, -20, - -10, 1, 1, 1, -1, -1, 1, -9, - -11, 1, 4, 11, 9, 5, -1, -10, - -9, 4, 4, 11, 9, 4, 6, -11, - -9, 1, 11, 11, 9, 9, 1, -11, - -9, 9, 11, 9, 10, 11, 11, -10, - -11, 4, 0, 1, 1, 1, 6, -9, - -19, -9, -11, -9, -11, -10, -10, -19, - }, - - // Piece square table for rook - { - 0, 0, 1, -1, 1, 1, -1, -1, - 6, 9, 11, 10, 11, 11, 11, 5, - -4, 0, 1, 1, -1, 1, -1, -4, - -6, 0, 1, -1, 1, 1, 1, -4, - -4, 1, 1, -1, 1, -1, -1, -6, - -5, 1, 0, -1, 1, 0, -1, -5, - -6, 0, -1, 0, 1, -1, -1, -6, - 0, 1, 1, 4, 6, 1, -1, 0, - }, - - // Piece square table for queens - { - -21, -10, -9, -6, -5, -10, -9, -19, - -11, 1, 1, -1, 1, -1, 0, -11, - -11, -1, 4, 5, 6, 5, 1, -9, - -4, 0, 5, 4, 5, 6, 0, -4, - -1, 1, 6, 4, 4, 6, -1, -4, - -11, 4, 4, 6, 6, 6, 1, -11, - -9, 1, 6, -1, 1, -1, 1, -9, - -19, -9, -11, -4, -6, -11, -10, -19, - }, - - // Piece square table for kings - { - -30, -40, -40, -50, -50, -40, -39, -30, - -30, -39, -39, -50, -51, -39, -40, -30, - -30, -39, -39, -50, -49, -39, -41, -31, - -31, -41, -41, -49, -49, -40, -40, -30, - -20, -31, -30, -40, -39, -29, -30, -20, - -10, -21, -20, -19, -20, -20, -20, -10, - 19, 21, 0, 0, 1, 1, 19, 19, - 19, 31, 9, 1, 0, 11, 30, 19, - }, -} - -var PSQT_EG [6][64]int = [6][64]int{ - - // Piece-square table for pawns - { - 0, 0, 0, 0, 0, 0, 0, 0, - -1, 50, 49, 49, 50, 50, 49, 1, - -1, 40, 41, 40, 40, 39, 39, 1, - -1, -1, -1, -1, 1, -1, 0, 1, - 0, -1, 1, -1, 0, -1, -1, 0, - -1, 1, -1, 1, 0, 1, 0, 0, - 0, 0, 1, 1, -1, 0, 1, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - }, - - // Piece-square table for knights - { - -51, -39, -29, -29, -30, -31, -41, -50, - -39, -19, 1, -1, 1, -1, -19, -41, - -30, 0, 11, 14, 15, 10, -1, -30, - -30, 4, 15, 21, 20, 14, 6, -29, - -31, -1, 16, 19, 19, 16, 0, -29, - -29, 4, 9, 16, 14, 11, 4, -29, - -40, -20, 0, 4, 5, -1, -21, -40, - -50, -40, -31, -31, -31, -30, -40, -50, - }, - - // Piece-square table for bishops - { - -19, -9, -9, -10, -11, -9, -11, -19, - -11, 0, -1, -1, 0, -1, 1, -9, - -10, 0, 6, 10, 9, 6, 1, -9, - -10, 4, 5, 9, 9, 6, 6, -11, - -10, 1, 10, 11, 9, 10, 1, -9, - -9, 9, 11, 10, 9, 9, 10, -11, - -10, 5, 0, 1, 0, 1, 4, -10, - -20, -9, -11, -9, -10, -9, -11, -21, - }, - - // Piece square table for rook - { - -1, 1, 1, -1, -1, 0, 1, 0, - -9, 0, 1, 0, 1, -1, 1, -11, - -10, 0, 0, 0, -1, 1, 1, -9, - -10, 1, 0, -1, 0, -1, 1, -9, - -11, 0, 0, -1, 1, 0, -1, -9, - -9, 0, 0, -1, 1, 0, -1, -10, - -11, 0, 1, 0, 0, -1, -1, -10, - 1, 1, 0, 0, 0, 0, -1, 0, - }, - - // Piece square table for queens - { - -21, -11, -9, -4, -5, -11, -9, -19, - -11, 1, 1, 1, 1, 1, 1, -10, - -11, -1, 6, 4, 6, 5, 1, -10, - -4, 1, 4, 4, 5, 5, 0, -4, - -1, 0, 5, 5, 5, 5, 0, -4, - -11, 4, 4, 5, 5, 6, 1, -11, - -10, 1, 6, 0, 0, -1, 1, -10, - -19, -9, -11, -5, -5, -9, -10, -19, - }, - - // Piece square table for kings - { - -50, -41, -30, -19, -19, -30, -39, -51, - -30, -20, -11, 0, 1, -9, -19, -29, - -31, -11, 21, 30, 30, 19, -11, -30, - -31, -10, 30, 41, 39, 31, -10, -30, - -29, -11, 30, 39, 39, 29, -9, -29, - -30, -10, 21, 31, 31, 20, -11, -31, - -30, -29, 1, 0, 1, 0, -30, -30, - -51, -29, -29, -30, -30, -31, -31, -51, - }, -} - -var FlipSq [2][64]int = [2][64]int{ - { - 0, 1, 2, 3, 4, 5, 6, 7, - 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, - 24, 25, 26, 27, 28, 29, 30, 31, - 32, 33, 34, 35, 36, 37, 38, 39, - 40, 41, 42, 43, 44, 45, 46, 47, - 48, 49, 50, 51, 52, 53, 54, 55, - 56, 57, 58, 59, 60, 61, 62, 63, - }, - - { - 56, 57, 58, 59, 60, 61, 62, 63, - 48, 49, 50, 51, 52, 53, 54, 55, - 40, 41, 42, 43, 44, 45, 46, 47, - 32, 33, 34, 35, 36, 37, 38, 39, - 24, 25, 26, 27, 28, 29, 30, 31, - 16, 17, 18, 19, 20, 21, 22, 23, - 8, 9, 10, 11, 12, 13, 14, 15, - 0, 1, 2, 3, 4, 5, 6, 7, - }, -} - -// Evaluate a board and give a score, from the perspective of the side to move ( -// more positive if it's good for the side to move, otherwise more negative). -func EvaluateBoard(board *engine.Board) int { - whiteScore := evaluateMaterial(board, engine.White) - whiteScore += evaluatePosition(board, engine.White) - - blackScore := evaluateMaterial(board, engine.Black) - blackScore += evaluatePosition(board, engine.Black) - - if board.ColorToMove == engine.White { - return whiteScore - blackScore - } - return blackScore - whiteScore -} - -// Evaluate the material a side has. -func evaluateMaterial(board *engine.Board, usColor uint8) (score int) { - score += board.PieceBB[usColor][engine.Pawn].CountBits() * PawnValue - score += board.PieceBB[usColor][engine.Knight].CountBits() * KnightValue - score += board.PieceBB[usColor][engine.Bishop].CountBits() * BishopValue - score += board.PieceBB[usColor][engine.Rook].CountBits() * RookValue - score += board.PieceBB[usColor][engine.Queen].CountBits() * QueenValue - return score -} - -// Evaluate a board position using piece-square tables. -func evaluatePosition(board *engine.Board, usColor uint8) int { - usBB := board.SideBB[usColor] - var opening, endgame int - - for usBB != 0 { - sq := usBB.PopBit() - pieceType := board.Squares[sq].Type - - opening += PSQT_MG[pieceType][FlipSq[usColor][sq]] - endgame += PSQT_EG[pieceType][FlipSq[usColor][sq]] - } - - phase := calcGamePhase(board) - return ((opening * (256 - phase)) + (endgame * phase)) / 256 -} - -// Calculate the current phase of the game -func calcGamePhase(board *engine.Board) int { - phase := TotalPhase - - phase -= PawnPhase * board.PieceBB[engine.White][engine.Pawn].CountBits() - phase -= KnightPhase * board.PieceBB[engine.White][engine.Knight].CountBits() - phase -= BishopPhase * board.PieceBB[engine.White][engine.Bishop].CountBits() - phase -= RookPhase * board.PieceBB[engine.White][engine.Rook].CountBits() - phase -= QueenPhase * board.PieceBB[engine.White][engine.Queen].CountBits() - - phase -= PawnPhase * board.PieceBB[engine.Black][engine.Pawn].CountBits() - phase -= KnightPhase * board.PieceBB[engine.Black][engine.Knight].CountBits() - phase -= BishopPhase * board.PieceBB[engine.Black][engine.Bishop].CountBits() - phase -= RookPhase * board.PieceBB[engine.Black][engine.Rook].CountBits() - phase -= QueenPhase * board.PieceBB[engine.Black][engine.Queen].CountBits() - - return (phase*256 + (TotalPhase / 2)) / TotalPhase -} diff --git a/ai/search.go b/ai/search.go deleted file mode 100644 index 1d94f40..0000000 --- a/ai/search.go +++ /dev/null @@ -1,284 +0,0 @@ -package ai - -import ( - "blunder/engine" - "fmt" - "time" -) - -const ( - SearchDepth = 50 - NullMove engine.Move = 0 - DrawValue = -(PawnValue / 2) - R = 2 - - TTMoveScore = 60 - FirstKillerMoveScore = 10 - SecondKillerMoveScore = 9 -) - -// A table used to implement Most-Valuable-Victim, -// Least-Valuabe-Attacker move scoring. -var MvvLva [7][6]int = [7][6]int{ - {16, 15, 14, 13, 12, 11}, // victim Pawn - {26, 25, 24, 23, 22, 21}, // victim Knight - {36, 35, 34, 33, 32, 31}, // victim Bishop - {46, 45, 44, 43, 42, 41}, // vitcim Rook - {56, 55, 54, 53, 52, 51}, // victim Queen - - {0, 0, 0, 0, 0, 0}, // victim King - {0, 0, 0, 0, 0, 0}, // No piece -} - -// A struct to hold state during a search -type Search struct { - Board engine.Board - Timer Timer - - killerMoves [SearchDepth + 1][2]engine.Move - nodesSearched uint64 - engineColor uint8 -} - -// Search for the best move for the side to move in the given position. -// Implemented using iterative deepening. -func (search *Search) Search() engine.Move { - search.engineColor = search.Board.ColorToMove - bestMove, bestScore := NullMove, NegInf - - search.Timer.StartSearch() - - for depth := 1; depth <= SearchDepth; depth++ { - searchTimeStart := time.Now() - move, score := search.rootNegamax(depth) - searchTimeEnd := time.Since(searchTimeStart) - - if search.Timer.TimeIsUp() { - break - } - - bestMove, bestScore = move, score - fmt.Printf( - "info depth %d score cp %d time %d nodes %d\n", - depth, bestScore, - searchTimeEnd/time.Millisecond, - search.nodesSearched, - ) - } - return bestMove -} - -// The top-level function for negamax, which returns a move and a score. -func (search *Search) rootNegamax(depth int) (engine.Move, int) { - search.nodesSearched = 0 - moves := engine.GenPseduoLegalMoves(&search.Board) - scores := search.scoreMoves(&moves, -1, NullMove) - - bestMove := NullMove - alpha, beta := NegInf, PosInf - for index := range moves { - orderMoves(index, &moves, &scores) - move := moves[index] - - search.Board.DoMove(move, true) - if search.Board.KingIsAttacked(search.Board.ColorToMove ^ 1) { - search.Board.UndoMove(move) - continue - } - - score := -search.negamax(depth-1, 0, -beta, -alpha) - search.Board.UndoMove(move) - - if score == beta { - return move, beta - } - - if score > alpha { - alpha = score - bestMove = move - } - } - return bestMove, alpha -} - -// The primary negamax function, which only returns a score and no best move. -func (search *Search) negamax(depth, ply, alpha, beta int) int { - if search.Timer.TimeIsUp() { - return 0 - } - - search.nodesSearched++ - inCheck := search.Board.KingIsAttacked(search.Board.ColorToMove) - - if inCheck { - depth++ - } - - if search.isDraw() { - return search.contempt() - } - - if depth == 0 { - return search.quiescence(alpha, beta) - } - - moves := engine.GenPseduoLegalMoves(&search.Board) - scores := search.scoreMoves(&moves, depth, NullMove) - - noMoves := true - for index := range moves { - orderMoves(index, &moves, &scores) - move := moves[index] - - search.Board.DoMove(move, true) - if search.Board.KingIsAttacked(search.Board.ColorToMove ^ 1) { - search.Board.UndoMove(move) - continue - } - - score := -search.negamax(depth-1, ply+1, -beta, -alpha) - search.Board.UndoMove(move) - noMoves = false - - if score >= beta { - search.storeKiller(depth, move) - return beta - } - - if score > alpha { - alpha = score - } - } - - if noMoves { - if inCheck { - alpha = NegInf + ply - } else { - alpha = search.contempt() - } - } - return alpha -} - -// An implementation of a quiesence search algorithm. -func (search *Search) quiescence(alpha, beta int) int { - if search.Timer.TimeIsUp() { - return 0 - } - - search.nodesSearched++ - standPat := EvaluateBoard(&search.Board) - - if standPat >= beta { - return beta - } - - if alpha < standPat { - alpha = standPat - } - - moves := engine.GenPseduoLegalCaptures(&search.Board) - scores := search.scoreMoves(&moves, -1, NullMove) - - for index := range moves { - orderMoves(index, &moves, &scores) - move := moves[index] - search.Board.DoMove(move, true) - if search.Board.KingIsAttacked(search.Board.ColorToMove ^ 1) { - search.Board.UndoMove(move) - continue - } - - score := -search.quiescence(-beta, -alpha) - search.Board.UndoMove(move) - - if score >= beta { - return beta - } - if score > alpha { - alpha = score - } - } - return alpha -} - -func (search *Search) isDraw() bool { - if search.Board.Rule50 >= 100 { - return true - } - return search.isRepition() -} - -// Determine if the current board state is being repeated. -func (search *Search) isRepition() bool { - var repPly uint16 - for repPly = 0; repPly < search.Board.RepitionPly; repPly++ { - if search.Board.Repitions[repPly] == search.Board.Hash { - return true - } - } - return false -} - -// Determine the draw score based on whose moving. If the engine is moving, -// return a negative score, and if the opponet is moving, return a positive -// score. -func (search *Search) contempt() int { - if search.Board.ColorToMove == search.engineColor { - return DrawValue - } - return -DrawValue -} - -// Given a "killer move" (a quiet move that caused a beta cut-off), store the -// Move in the slot for the given depth. -func (search *Search) storeKiller(depth int, move engine.Move) { - if move.MoveType() != engine.Attack && move.MoveType() != engine.AttackEP { - if move != search.killerMoves[depth][0] { - search.killerMoves[depth][1] = search.killerMoves[depth][0] - search.killerMoves[depth][0] = move - } - } -} - -// Score the moves given -func (search *Search) scoreMoves(moves *engine.Moves, depth int, ttMove engine.Move) (scores []int) { - scores = make([]int, len(*moves)) - for index, move := range *moves { - captured := &search.Board.Squares[move.ToSq()] - moved := &search.Board.Squares[move.FromSq()] - scores[index] = MvvLva[captured.Type][moved.Type] - - if move == ttMove { - scores[index] = TTMoveScore - } else if depth > 0 && move == search.killerMoves[depth][0] { - scores[index] = FirstKillerMoveScore - } else if depth > 0 && move == search.killerMoves[depth][1] { - scores[index] = SecondKillerMoveScore - } - } - return scores -} - -// Order the moves given by finding the best move and putting it -// at the index given. -func orderMoves(index int, moves *engine.Moves, scores *[]int) { - bestIndex := index - bestScore := (*scores)[bestIndex] - - for index := bestIndex; index < len(*moves); index++ { - if (*scores)[index] > bestScore { - bestIndex = index - bestScore = (*scores)[index] - } - } - - tempMove := (*moves)[index] - tempScore := (*scores)[index] - - (*moves)[index] = (*moves)[bestIndex] - (*scores)[index] = (*scores)[bestIndex] - - (*moves)[bestIndex] = tempMove - (*scores)[bestIndex] = tempScore -} diff --git a/ai/time.go b/ai/time.go deleted file mode 100644 index 73fc297..0000000 --- a/ai/time.go +++ /dev/null @@ -1,36 +0,0 @@ -package ai - -import "time" - -type Timer struct { - timeLeft int64 - increment int64 - timeForMove int64 - startTime time.Time -} - -func (timer *Timer) UpdateInternals(timeLeft, increment int64) { - timer.timeLeft = timeLeft - timer.increment = increment -} - -func (timer *Timer) StartSearch() { - timer.timeForMove = (timer.timeLeft / 40) + (timer.increment / 2) - if timer.timeForMove >= timer.timeLeft { - timer.timeForMove -= 500 - } - - if timer.timeForMove <= 0 { - timer.timeForMove = 100 - } - - timer.startTime = time.Now() -} - -func (timer *Timer) TimeIsUp() bool { - return int64(time.Since(timer.startTime)/time.Millisecond) > timer.timeForMove -} - -func (timer *Timer) TimeTaken() int64 { - return int64(time.Since(timer.startTime) / time.Millisecond) -} diff --git a/blunder/main.go b/blunder/main.go index 890b4bf..ef654ce 100644 --- a/blunder/main.go +++ b/blunder/main.go @@ -1,47 +1,46 @@ package main import ( - "blunder/ui" - "bufio" + "blunder/engine" + "flag" "fmt" + "log" "os" - "strings" + "runtime" + "runtime/pprof" ) -const HelpMessage = ` -Options: -- uci: Begin Blunder's UCI protocol -- cli: Enter Blunder's command-line interface -- help: Quit the program -` +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") -func mainLoop() { - for { - reader := bufio.NewReader(os.Stdin) - programMode, _ := reader.ReadString('\n') - programMode = strings.Replace(programMode, "\r\n", "\n", -1) - - //fmt.Printf("HERE: %#v\n", programMode) - //fmt.Printf("HERE: %#v\n", strings.Replace(programMode, "\r\n", "\n", -1)) - - if programMode == "uci\n" || programMode == "uci" { - ui.UCILoop() - break - } else if programMode == "cli\n" { - ui.CmdLoop() - break - } else if programMode == "quit\n" { - break - } else if programMode == "help\n" { - fmt.Println(HelpMessage) - } else { - fmt.Printf("\nUnknown command \"%v\"\n", strings.TrimSuffix(programMode, "\n")) - fmt.Printf("Enter \"help\" to show available commands\n\n") +func main() { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal(err) } + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() } -} + /*var s engine.Search + s.Pos.LoadFEN(engine.FENStartPosition) + s.Timer.TimeLeft = 600000 + s.TransTable.Resize(engine.DefaultTTSize) -func main() { - mainLoop() + start := time.Now() + m := s.Search() + fmt.Println("Bestmove:", m) + elapsed := time.Since(start) + fmt.Printf("Time: %vms\n", elapsed.Milliseconds())*/ + + defer func() { + if r := recover(); r != nil { + println(fmt.Sprintf("Internal error: %v", r)) + buf := make([]byte, 1<<16) + stackSize := runtime.Stack(buf, true) + println(fmt.Sprintf("%s\n", string(buf[0:stackSize]))) + } + }() + engine.UCILoop() } diff --git a/engine/bitboard.go b/engine/bitboard.go index 8197a22..04966ee 100644 --- a/engine/bitboard.go +++ b/engine/bitboard.go @@ -5,54 +5,30 @@ import ( "math/bits" ) -// bitboard.go defines the bitboard type and its various methods. - -var SquareBB [65]Bitboard - -func init() { - for sq := 0; sq < 65; sq++ { - SquareBB[sq] = 0x8000000000000000 >> sq - } -} +// bitboard.go contains the implementation of a bitboard datatype for the engine. +// A type representing a bitboard, which is a unsigned 64-bit number. Blunder's +// bitboard representation has the most significant bit being A1 and the least signficanrt +// bit being H8. type Bitboard uint64 -// Pretty print a bitboard. -func (bb Bitboard) String() (boardStr string) { - bitstring := fmt.Sprintf("%064b\n", bb) - boardStr += "\n" - for rankStartPos := 56; rankStartPos >= 0; rankStartPos -= 8 { - boardStr += fmt.Sprintf("%v | ", (rankStartPos/8)+1) - for index := rankStartPos; index < rankStartPos+8; index++ { - squareChar := bitstring[index] - if squareChar == '0' { - squareChar = '.' - } - boardStr += fmt.Sprintf("%c ", squareChar) - } - boardStr += "\n" - } - boardStr += " " - for fileNo := 0; fileNo < 8; fileNo++ { - boardStr += "--" - } +// A constant representing a bitboard with every square set +const FullBB Bitboard = 0xffffffffffffffff - boardStr += "\n " - for _, file := range "abcdefgh" { - boardStr += fmt.Sprintf("%c ", file) - } - boardStr += "\n" - return boardStr -} +// A global constant where each entry represents a square on the chess board, +// and each entry contains a bitboard with the bit set high at that square. +// An extra entry is given so that the invalid square constant NoSq can be +// indexed into the table without the program crashing. +var SquareBB [64]Bitboard -// Set the bit of the given bitbord at the given position. -func (bb *Bitboard) SetBit(sq uint8) { - *bb |= SquareBB[sq] +// Set the bit at given square. +func (bitboard *Bitboard) SetBit(sq uint8) { + *bitboard |= SquareBB[sq] } -// Clear the bit of the given bitbord at the given position. -func (bb *Bitboard) ClearBit(sq uint8) { - *bb &= ^SquareBB[sq] +// Clear the bit at given square. +func (bitboard *Bitboard) ClearBit(sq uint8) { + *bitboard &= ^SquareBB[sq] } // Test whether the bit of the given bitbord at the given @@ -62,25 +38,56 @@ func (bb Bitboard) BitSet(sq uint8) bool { } // Get the position of the MSB of the given bitboard. -func (bb Bitboard) Msb() uint8 { - return uint8(bits.LeadingZeros64(uint64(bb))) +func (bitboard Bitboard) Msb() uint8 { + return uint8(bits.LeadingZeros64(uint64(bitboard))) } // Get the position of the LSB of the given bitboard, // a bitboard with only the LSB set, and clear the LSB. -func (bb *Bitboard) PopBit() uint8 { - sq := bb.Msb() - bb.ClearBit(sq) +func (bitboard *Bitboard) PopBit() uint8 { + sq := bitboard.Msb() + bitboard.ClearBit(sq) return sq } // Count the bits in a given bitboard using the SWAR-popcount // algorithm for 64-bit integers. -func (bb Bitboard) CountBits() int { - u := uint64(bb) - u = u - ((u >> 1) & 0x5555555555555555) - u = (u & 0x3333333333333333) + ((u >> 2) & 0x3333333333333333) - u = (u + (u >> 4)) & 0x0f0f0f0f0f0f0f0f - u = (u * 0x0101010101010101) >> 56 - return int(u) +func (bitboard Bitboard) CountBits() int { + return bits.OnesCount64(uint64(bitboard)) +} + +// Return a string representation of the given bitboard +func (bitboard Bitboard) String() (bitboardAsString string) { + bitstring := fmt.Sprintf("%064b\n", bitboard) + bitboardAsString += "\n" + for rankStartPos := 56; rankStartPos >= 0; rankStartPos -= 8 { + bitboardAsString += fmt.Sprintf("%v | ", (rankStartPos/8)+1) + for index := rankStartPos; index < rankStartPos+8; index++ { + squareChar := bitstring[index] + if squareChar == '0' { + squareChar = '.' + } + bitboardAsString += fmt.Sprintf("%c ", squareChar) + } + bitboardAsString += "\n" + } + bitboardAsString += " " + for fileNo := 0; fileNo < 8; fileNo++ { + bitboardAsString += "--" + } + + bitboardAsString += "\n " + for _, file := range "abcdefgh" { + bitboardAsString += fmt.Sprintf("%c ", file) + } + bitboardAsString += "\n" + return bitboardAsString +} + +// Initalize the bitboard constants. +func init() { + var sq uint8 + for sq = 0; sq < 64; sq++ { + SquareBB[sq] = 0x8000000000000000 >> sq + } } diff --git a/engine/board.go b/engine/board.go index 3e44b3f..2285ff3 100644 --- a/engine/board.go +++ b/engine/board.go @@ -7,9 +7,10 @@ import ( "unicode" ) -// A file containg the implementation of Blunder's internal board representation. - const ( + // Constants representing each piece type. The value of the constants + // are selected so they can be used in Position.PieceBB to index the + // bitboards representing the given piece. Pawn uint8 = 0 Knight uint8 = 1 Bishop uint8 = 2 @@ -18,30 +19,26 @@ const ( King uint8 = 5 NoType uint8 = 6 + // Constants representing each piece color. The value of the constants + // are selected so they can be used in Position.PieceBB and Position.SideBB to + // index the bitboards representing the given piece of the given color, or + // the given color. Black uint8 = 0 White uint8 = 1 NoColor uint8 = 2 - MaxHistory uint16 = 100 - MaxGamePlies uint16 = 1024 - - NoEPSquare uint8 = 64 - FENStartPosition = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 0" - FENKiwiPete = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1" + // Constants representing the four castling rights. Each constant is set to a number + // with a single high bit, corresponding to each castling right. + WhiteKingsideRight uint8 = 0x8 + WhiteQueensideRight uint8 = 0x4 + BlackKingsideRight uint8 = 0x2 + BlackQueensideRight uint8 = 0x1 - WhiteKingside Bitboard = 0x900000000000000 - WhiteQueenside Bitboard = 0x8800000000000000 - BlackKingside Bitboard = 0x9 - BlackQueenside Bitboard = 0x88 + // Common fen strings used in debugging and initalizing the engine. + FENStartPosition = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 0" + FENKiwiPete = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1" - CastleWKSRand64 uint16 = 768 - CastleWQSRand64 uint16 = 769 - CastleBKSRand64 uint16 = 770 - CastleBQSRand64 uint16 = 771 - EPRand64 uint16 = 772 - SideToMoveRand64 uint16 = 780 -) -const ( + // Constants mapping each board coordinate to its square A1, B1, C1, D1, E1, F1, G1, H1 = 0, 1, 2, 3, 4, 5, 6, 7 A2, B2, C2, D2, E2, F2, G2, H2 = 8, 9, 10, 11, 12, 13, 14, 15 A3, B3, C3, D3, E3, F3, G3, H3 = 16, 17, 18, 19, 20, 21, 22, 23 @@ -50,8 +47,45 @@ const ( A6, B6, C6, D6, E6, F6, G6, H6 = 40, 41, 42, 43, 44, 45, 46, 47 A7, B7, C7, D7, E7, F7, G7, H7 = 48, 49, 50, 51, 52, 53, 54, 55 A8, B8, C8, D8, E8, F8, G8, H8 = 56, 57, 58, 59, 60, 61, 62, 63 + + // A constant representing no square + NoSq = 64 + + // Constant representing north and south deltas on the board + NorthDelta = 8 + SouthDelta = -8 + + // A constant representing the maximum game ply, + // used to initalize the array for holding repetition + // detection history. + MaxGamePly = 1024 ) +// A 64 element array where each entry, when bitwise ANDed with the +// castling rights, destorys the correct bit in the castling rights +// if a move to or from that square would take away castling rights. +var Spoilers [64]uint8 = [64]uint8{ + 0xb, 0xf, 0xf, 0xf, 0x3, 0xf, 0xf, 0x7, + 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, + 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, + 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, + 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, + 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, + 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, + 0xe, 0xf, 0xf, 0xf, 0xc, 0xf, 0xf, 0xd, +} + +// An array mapping a castling kings destination square, +// to the origin and destination square of the appropriate +// rook to move. +var CastlingRookSq map[uint8][2]uint8 = map[uint8][2]uint8{ + G1: {H1, F1}, + C1: {A1, D1}, + G8: {H8, F8}, + C8: {A8, D8}, +} + +// A constant mapping piece characters to Piece objects. var CharToPiece map[byte]Piece = map[byte]Piece{ 'P': {Pawn, White}, 'N': {Knight, White}, @@ -67,8 +101,9 @@ var CharToPiece map[byte]Piece = map[byte]Piece{ 'k': {King, Black}, } +// A constant mapping piece types to their respective characters. var PieceTypeToChar map[uint8]rune = map[uint8]rune{ - Pawn: 'p', + Pawn: 'i', Knight: 'n', Bishop: 'b', Rook: 'r', @@ -77,51 +112,349 @@ var PieceTypeToChar map[uint8]rune = map[uint8]rune{ NoType: '.', } -var EPDelta [2]int8 = [2]int8{8, -8} - +// A struct representing a piece type Piece struct { Type uint8 Color uint8 } -type BoardState struct { - Moved Piece - Captured Piece - CastlingRights Bitboard +// A struct representing position state that is irreversible (cannot be undone in +// UnmakeMove). A position state object is used each time a move is made, and then popped +// off of a stack once a move needs to be unmade. +type State struct { + CastlingRights uint8 + EPSq uint8 Rule50 uint8 - EPSquare uint8 + + Captured Piece + Moved Piece } -type Board struct { +// A struct reprenting Blunder's core internal position representation, which consists +// of 12 bitboards for each piece type, 2 bitboards for each color, 64-square +// mailbox representation of the board for easy accsess to square-centric information, +// and several other state keeping fields (enpassant square, side to move, etc.) +type Position struct { PieceBB [2][6]Bitboard SideBB [2]Bitboard Squares [64]Piece - KingPos [2][6]uint8 - ColorToMove uint8 - GamePly uint16 - Rule50 uint8 + // The castling rights are keep track of using 4-bits: + // 00001000 = white kingside castling right + // 00000100 = white queenside castling right + // 00000010 = black kingside castling right + // 00000001 = black queenside castling right + CastlingRights uint8 + + // The zobrist hash of the position + Hash uint64 + + SideToMove uint8 + EPSq uint8 + + Ply uint16 + Rule50 uint8 + + prevStates [100]State + StatePly uint8 + + History [MaxGamePly]uint64 + HistoryPly uint16 +} + +func (pos *Position) MakeMove(move Move) bool { + // Get the data we need from the given move + from := move.FromSq() + to := move.ToSq() + moveType := move.MoveType() + flag := move.Flag() + + // Create a new State object to save the state of the irreversible aspects + // of the position before making the current move. + state := State{ + CastlingRights: pos.CastlingRights, + EPSq: pos.EPSq, + Rule50: pos.Rule50, + Captured: pos.Squares[to], + Moved: pos.Squares[from], + } + + // Increment the game ply and the fifty-move rule counter + pos.Ply++ + pos.Rule50++ + + // Clear the en passant square and en passant zobrist number + pos.Hash ^= Zobrist.EPNumber(pos.EPSq) + pos.EPSq = NoSq + + // Clear the moving piece from its origin square + pos.clearPiece(from) + + if moveType == Quiet { + // if the move is quiet, simple put the piece at the destination square. + pos.putPiece(state.Moved.Type, state.Moved.Color, to) + } else if moveType == Attack { + if flag == AttackEP { + // If it's an attack en passant, get the actually capture square + // of the pawn being captured, remove it, and put the moving pawn + // on the destination square... + capSq := uint8(int8(to) - pawnPush(pos.SideToMove)) + state.Captured = pos.Squares[capSq] + + pos.clearPiece(capSq) + pos.putPiece(Pawn, pos.SideToMove, to) + + } else { + // Otherwise if the move is a normal attack, remove the captured piece + // from the position, and put the moving piece at its destination square... + pos.clearPiece(to) + pos.putPiece(state.Moved.Type, state.Moved.Color, to) + } + + // and reset the fifty-move rule counter. + pos.Rule50 = 0 + } else if moveType == Castle { + // If the move is a castle, move the king to the appropriate square... + pos.putPiece(state.Moved.Type, state.Moved.Color, to) + + // And move the correct rook. + rookFrom, rookTo := CastlingRookSq[to][0], CastlingRookSq[to][1] + pos.clearPiece(rookFrom) + pos.putPiece(Rook, pos.SideToMove, rookTo) + + } + + if state.Moved.Type == Pawn { + // If a pawn is moving, do some extra work. + + // Reset the fifty-move rule counter. + pos.Rule50 = 0 + + if moveType == Promotion { + // If a pawn is promoting, check if it's capturing a piece, + // remove the captured piece if needed, and then put the + // correct promotion piece type on the to square indicated + // by the move flag value. + if state.Captured.Type != NoType { + pos.clearPiece(to) + } + pos.putPiece(uint8(flag+1), pos.SideToMove, to) + } + + if abs(int16(from)-int16(to)) == 16 { + // If the move is a double pawn push, and there is no enemy pawn that's in + // a position to capture en passant on the next turn, don't set the position's + // en passant square. + + pos.EPSq = uint8(int8(to) - pawnPush(pos.SideToMove)) + if PawnAttacks[pos.SideToMove][pos.EPSq]&pos.PieceBB[pos.SideToMove^1][Pawn] == 0 { + pos.EPSq = NoSq + } + } + + } - EPSquare uint8 - CastlingRights Bitboard - Hash uint64 + // Remove the current castling rights. + pos.Hash ^= Zobrist.CastlingNumber(pos.CastlingRights) - history [MaxHistory]BoardState - historyPly uint16 + // Update the castling rights and the zobrist hash with the new castling rights. + pos.CastlingRights = pos.CastlingRights & Spoilers[from] & Spoilers[to] + pos.Hash ^= Zobrist.CastlingNumber(pos.CastlingRights) - Repitions [MaxGamePlies]uint64 - RepitionPly uint16 + // Update the zobrist hash if the en passant square was set + pos.Hash ^= Zobrist.EPNumber(pos.EPSq) + + // Save the State object and increment the stack counter + // to point to the next empty slot in the position state history. + pos.prevStates[pos.StatePly] = state + pos.StatePly++ + + // Flip the side to move and update the zobrist hash + pos.SideToMove ^= 1 + pos.Hash ^= Zobrist.SideToMoveNumber(pos.SideToMove) + + // Save the current zobrist in the position history array. + pos.HistoryPly++ + pos.History[pos.HistoryPly] = pos.Hash + + // Test if the move was legal or not, and let the caller know. + return !sqIsAttacked(pos, pos.SideToMove^1, pos.PieceBB[pos.SideToMove^1][King].Msb()) +} + +func (pos *Position) UnmakeMove(move Move) { + // Get the State object for this move + pos.StatePly-- + state := pos.prevStates[pos.StatePly] + + // remove the en passant zobrist number if there was one in the position + // we're undoing, and remove the castling rights zobrist number. + pos.Hash ^= Zobrist.EPNumber(pos.EPSq) + pos.Hash ^= Zobrist.CastlingNumber(pos.CastlingRights) + + // Remove the current positions from the position history + pos.HistoryPly-- + + // Restore the irreversible aspects of the position using the State object. + pos.CastlingRights = state.CastlingRights + pos.EPSq = state.EPSq + pos.Rule50 = state.Rule50 + + // Update the zobrist hash with the restored values castling rights + // and en passant square. + pos.Hash ^= Zobrist.CastlingNumber(pos.CastlingRights) + pos.Hash ^= Zobrist.EPNumber(pos.EPSq) + + // Flip the side to move and update the zobrist hash + pos.SideToMove ^= 1 + pos.Hash ^= Zobrist.SideToMoveNumber(pos.SideToMove) + + // Decrement the game ply + pos.Ply-- + + // Get the data we need from the given move + from := move.FromSq() + to := move.ToSq() + moveType := move.MoveType() + flag := move.Flag() + + // Put the moving piece back on it's orgin square + pos.putPiece(state.Moved.Type, state.Moved.Color, from) + + if moveType == Quiet { + // if the move is quiet, remove the piece from its destination square. + pos.clearPiece(to) + } else if moveType == Attack { + if flag == AttackEP { + // If it was an attack en passant, put the pawn back that + // was captured, and clear the moving pawn from the destination + // square. + capSq := uint8(int8(to) - pawnPush(pos.SideToMove)) + pos.clearPiece(to) + pos.putPiece(Pawn, state.Captured.Color, capSq) + } else { + // Otherwise If the move was a normal attack, put the captured piece + // back on the destination square, and remove the attacking piece. + pos.clearPiece(to) + pos.putPiece(state.Captured.Type, state.Captured.Color, to) + } + } else if moveType == Castle { + // If the move was a castle, clear the king from the destination square... + pos.clearPiece(to) + + // and move the castled rook back to the right square. + rookFrom, rookTo := CastlingRookSq[to][0], CastlingRookSq[to][1] + pos.clearPiece(rookTo) + pos.putPiece(Rook, pos.SideToMove, rookFrom) + } + + if state.Moved.Type == Pawn { + // If a pawn was moving, do some extra work. + + if moveType == Promotion { + // If the pawn was promoted, remove the promoted piece, and if + // the promotion was a capture, put the captured piece back on + // the destination square. + pos.clearPiece(to) + if state.Captured.Type != NoType { + pos.putPiece(state.Captured.Type, state.Captured.Color, to) + } + } + } +} + +// Make a "null"-move for null-move pruning: +// https://www.chessprogramming.org/Null_Move_Pruning +// +func (pos *Position) MakeNullMove() { + state := State{ + CastlingRights: pos.CastlingRights, + EPSq: pos.EPSq, + Rule50: pos.Rule50, + } + + // Save the State object and increment the stack counter + // to point to the next empty slot in the position state history. + pos.prevStates[pos.StatePly] = state + pos.StatePly++ + + // Clear the en passant square and en passant zobrist number + pos.Hash ^= Zobrist.EPNumber(pos.EPSq) + pos.EPSq = NoSq + + // Set the fifty move rule counter to 0, since we're + // making a null-move. + pos.Rule50 = 0 + + // Increment the game ply. + pos.Ply++ + + // Flip the side to move and update the zobrist hash + pos.SideToMove ^= 1 + pos.Hash ^= Zobrist.SideToMoveNumber(pos.SideToMove) + + // Save the current zobrist in the position history array. + pos.HistoryPly++ + pos.History[pos.HistoryPly] = pos.Hash + +} + +func (pos *Position) UnmakeNullMove() { + // Get the State object for the null move + pos.StatePly-- + state := pos.prevStates[pos.StatePly] + + // Restore the irreversible aspects of the position using the State object. + pos.CastlingRights = state.CastlingRights + pos.EPSq = state.EPSq + pos.Rule50 = state.Rule50 + + // Decrement the game ply. + pos.Ply-- + + // Update the zobrist hash with the restored en passant square. + pos.Hash ^= Zobrist.EPNumber(pos.EPSq) + + // Flip the side to move and update the zobrist hash + pos.SideToMove ^= 1 + pos.Hash ^= Zobrist.SideToMoveNumber(pos.SideToMove) + + // Remove the current positions from the position history + pos.HistoryPly-- +} + +// Put the piece given on the given square +func (pos *Position) putPiece(pieceType, pieceColor, to uint8) { + pos.PieceBB[pieceColor][pieceType].SetBit(to) + pos.SideBB[pieceColor].SetBit(to) + pos.Squares[to].Type = pieceType + pos.Squares[to].Color = pieceColor + pos.Hash ^= Zobrist.PieceNumber(pieceType, pieceColor, to) +} + +// Clear the piece given from the given square. +func (pos *Position) clearPiece(from uint8) { + piece := &pos.Squares[from] + pos.PieceBB[piece.Color][piece.Type].ClearBit(from) + pos.SideBB[piece.Color].ClearBit(from) + + pos.Hash ^= Zobrist.PieceNumber(piece.Type, piece.Color, from) + piece.Type = NoType + piece.Color = NoColor } -// Setup a new Board with the internal fields set using a -// Forsyth–Edwards Notation (FEN) line. -func (board *Board) LoadFEN(fen string) { - board.PieceBB = [2][6]Bitboard{} - board.SideBB = [2]Bitboard{} - board.Squares = [64]Piece{} - board.CastlingRights = 0 - board.Hash = 0 +// Load in a FEN string and use it to setup the position. +func (pos *Position) LoadFEN(fen string) { + // Reset the internal fields of the position + pos.PieceBB = [2][6]Bitboard{} + pos.SideBB = [2]Bitboard{} + pos.Squares = [64]Piece{} + pos.CastlingRights = 0 + + for square := range pos.Squares { + pos.Squares[square] = Piece{Type: NoType, Color: NoColor} + } + // Load in each field of the FEN string. fields := strings.Fields(fen) pieces := fields[0] color := fields[1] @@ -130,24 +463,15 @@ func (board *Board) LoadFEN(fen string) { halfMove := fields[4] fullMove := fields[5] - for square := range board.Squares { - board.Squares[square] = Piece{Type: NoType, Color: NoColor} - } - + // Loop over each square of the board, rank by rank, from left to right, + // loading in pieces at squares described by the FEN string. for index, sq := 0, 56; index < len(pieces); index++ { char := pieces[index] switch char { case 'p', 'n', 'b', 'r', 'q', 'k', 'P', 'N', 'B', 'R', 'Q', 'K': piece := CharToPiece[char] - board.putPiece(piece.Type, piece.Color, uint8(sq)) - - if char == 'K' { - board.KingPos[White][King] = uint8(sq) - } else if char == 'k' { - board.KingPos[Black][King] = uint8(sq) - } - - board.Squares[sq] = piece + pos.putPiece(piece.Type, piece.Color, uint8(sq)) + pos.Squares[sq] = piece sq++ case '/': sq -= 16 @@ -156,390 +480,120 @@ func (board *Board) LoadFEN(fen string) { } } - board.ColorToMove = Black + // Set the side to move for the position. + pos.SideToMove = Black if color == "w" { - board.ColorToMove = White - } - - if board.ColorToMove == White { - board.Hash ^= Random64[SideToMoveRand64] + pos.SideToMove = White } - board.EPSquare = NoEPSquare + // Set the en passant square for the position. + pos.EPSq = NoSq if ep != "-" { - board.EPSquare = CoordinateToPos(ep) - if (PawnAttacks[board.ColorToMove^1][board.EPSquare] & board.PieceBB[board.ColorToMove][Pawn]) == 0 { - board.EPSquare = NoEPSquare - } - - if board.EPSquare != NoEPSquare { - board.Hash ^= Random64[EPRand64+uint16(FileOf(board.EPSquare))] + pos.EPSq = coordinateToPos(ep) + if (PawnAttacks[pos.SideToMove^1][pos.EPSq] & pos.PieceBB[pos.SideToMove][Pawn]) == 0 { + pos.EPSq = NoSq } } + // Set the half move counter and game ply for the position. halfMoveCounter, _ := strconv.Atoi(halfMove) - board.Rule50 = uint8(halfMoveCounter) + pos.Rule50 = uint8(halfMoveCounter) gamePly, _ := strconv.Atoi(fullMove) gamePly *= 2 - if board.ColorToMove == Black { + if pos.SideToMove == Black { gamePly-- } - board.GamePly = uint16(gamePly) + pos.Ply = uint16(gamePly) + // Set the castling rights, for the position. for _, char := range castling { switch char { case 'K': - board.CastlingRights.SetBit(E1) - board.CastlingRights.SetBit(H1) - board.Hash ^= Random64[CastleWKSRand64] + pos.CastlingRights |= WhiteKingsideRight case 'Q': - board.CastlingRights.SetBit(E1) - board.CastlingRights.SetBit(A1) - board.Hash ^= Random64[CastleWQSRand64] + pos.CastlingRights |= WhiteQueensideRight case 'k': - board.CastlingRights.SetBit(E8) - board.CastlingRights.SetBit(H8) - board.Hash ^= Random64[CastleBKSRand64] + pos.CastlingRights |= BlackKingsideRight case 'q': - board.CastlingRights.SetBit(E8) - board.CastlingRights.SetBit(A8) - board.Hash ^= Random64[CastleBQSRand64] + pos.CastlingRights |= BlackQueensideRight } } - board.RepitionPly = 0 - board.Repitions[board.RepitionPly] = board.Hash -} - -// Get the random 64-bit number corresponding to the given piece -// of a certian color and type, on a certian square. -func getPieceHash(pieceType, pieceColor uint8, sq uint8) uint64 { - if pieceColor == White { - return Random64[(uint16(pieceType)*2+1)*64+uint16(sq)] - } - return Random64[(uint16(pieceType)*2)*64+uint16(sq)] + // Generate the zobrist hash for the position... + pos.Hash = 0 + pos.Hash = Zobrist.GenHash(pos) + // and add the hash as the first entry in the position history. + pos.HistoryPly = 0 + pos.History[pos.HistoryPly] = pos.Hash } -// hash the castling rights into, or out of, the current board Zobrist -// hash. A BoardState object is needed to figure how the castling rights -// changed, and how they need to be updated. -func (board *Board) hashCastlingRights(state *BoardState) { - if board.CastlingRights != state.CastlingRights { - if state.CastlingRights&WhiteKingside == WhiteKingside && - board.CastlingRights&WhiteKingside != WhiteKingside { - - board.Hash ^= Random64[CastleWKSRand64] - } - if state.CastlingRights&WhiteQueenside == WhiteQueenside && - board.CastlingRights&WhiteQueenside != WhiteQueenside { - board.Hash ^= Random64[CastleWQSRand64] - } - if state.CastlingRights&BlackKingside == BlackKingside && - board.CastlingRights&BlackKingside != BlackKingside { - board.Hash ^= Random64[CastleBKSRand64] - } - if state.CastlingRights&BlackQueenside == BlackQueenside && - board.CastlingRights&BlackQueenside != BlackQueenside { - board.Hash ^= Random64[CastleBQSRand64] - } - } -} - -// Return a pretty string representation of the board. Useful for debugging -// and command-line interaction purposes. -func (board Board) String() (str string) { - str += "\n" +// Return a string representation of the board. +func (pos Position) String() (boardAsString string) { + boardAsString += "\n" for rankStartPos := 56; rankStartPos >= 0; rankStartPos -= 8 { - str += fmt.Sprintf("%v | ", (rankStartPos/8)+1) + boardAsString += fmt.Sprintf("%v | ", (rankStartPos/8)+1) for index := rankStartPos; index < rankStartPos+8; index++ { - piece := board.Squares[index] + piece := pos.Squares[index] pieceChar := PieceTypeToChar[piece.Type] if piece.Color == White { pieceChar = unicode.ToUpper(pieceChar) } - str += fmt.Sprintf("%c ", pieceChar) + boardAsString += fmt.Sprintf("%c ", pieceChar) } - str += "\n" + boardAsString += "\n" } - str += " " + boardAsString += " " for fileNo := 0; fileNo < 8; fileNo++ { - str += "--" + boardAsString += "--" } - str += "\n " + boardAsString += "\n " for _, file := range "abcdefgh" { - str += fmt.Sprintf("%c ", file) + boardAsString += fmt.Sprintf("%c ", file) } - str += "\n\n" - if board.ColorToMove == White { - str += "turn: white\n" + boardAsString += "\n\n" + if pos.SideToMove == White { + boardAsString += "turn: white\n" } else { - str += "turn: black\n" + boardAsString += "turn: black\n" } - str += "castling rights: " - if board.CastlingRights&WhiteKingside == WhiteKingside { - str += "K" + boardAsString += "castling rights: " + if pos.CastlingRights&WhiteKingsideRight != 0 { + boardAsString += "K" } - if board.CastlingRights&WhiteQueenside == WhiteQueenside { - str += "Q" + if pos.CastlingRights&WhiteQueensideRight != 0 { + boardAsString += "Q" } - if board.CastlingRights&BlackKingside == BlackKingside { - str += "k" + if pos.CastlingRights&BlackKingsideRight != 0 { + boardAsString += "k" } - if board.CastlingRights&BlackQueenside == BlackQueenside { - str += "q" + if pos.CastlingRights&BlackQueensideRight != 0 { + boardAsString += "q" } - str += "\nen passant: " - if board.EPSquare == NoEPSquare { - str += "none" + boardAsString += "\nen passant: " + if pos.EPSq == NoSq { + boardAsString += "none" } else { - str += PosToCoordinate(board.EPSquare) + boardAsString += posToCoordinate(pos.EPSq) } - str += fmt.Sprintf("\nzobrist hash: 0x%x\n", board.Hash) - str += fmt.Sprintf("\nrule 50: %d\n", board.Rule50) - str += fmt.Sprintf("game ply: %d\n", board.GamePly) - return str + boardAsString += fmt.Sprintf("\nzobrist hash: 0x%x", pos.Hash) + boardAsString += fmt.Sprintf("\nrule 50: %d\n", pos.Rule50) + boardAsString += fmt.Sprintf("game ply: %d\n", pos.Ply) + return boardAsString } -func (board *Board) DoMove(move Move, saveState bool) { - from, to, movType := move.FromSq(), move.ToSq(), move.MoveType() - state := &board.history[board.historyPly] - - if saveState { - board.historyPly++ +// Given a color, return the delta for a single pawn push for that +// color. +func pawnPush(color uint8) int8 { + if color == White { + return NorthDelta } - - state.Moved = board.Squares[from] - state.Captured = board.Squares[to] - state.CastlingRights = board.CastlingRights - state.Rule50 = board.Rule50 - state.EPSquare = board.EPSquare - - if board.EPSquare != NoEPSquare { - board.Hash ^= Random64[EPRand64+uint16(FileOf(board.EPSquare))] - } - - board.EPSquare = NoEPSquare - - board.Rule50++ - board.GamePly++ - - switch movType { - case CastleWKS: - board.movePiece(E1, G1) - board.movePiece(H1, F1) - case CastleWQS: - board.movePiece(E1, C1) - board.movePiece(A1, D1) - case CastleBKS: - board.movePiece(E8, G8) - board.movePiece(H8, F8) - case CastleBQS: - board.movePiece(E8, C8) - board.movePiece(A8, D8) - case KnightPromotion: - board.removePiece(from) - if state.Captured.Type != NoType { - board.removePiece(to) - } - board.putPiece(Knight, board.ColorToMove, to) - case BishopPromotion: - board.removePiece(from) - if state.Captured.Type != NoType { - board.removePiece(to) - } - board.putPiece(Bishop, board.ColorToMove, to) - case RookPromotion: - board.removePiece(from) - if state.Captured.Type != NoType { - board.removePiece(to) - } - board.putPiece(Rook, board.ColorToMove, to) - case QueenPromotion: - board.removePiece(from) - if state.Captured.Type != NoType { - board.removePiece(to) - } - board.putPiece(Queen, board.ColorToMove, to) - case AttackEP: - capturePos := uint8(int8(to) + EPDelta[board.ColorToMove]) - state.Captured = board.Squares[capturePos] - board.removePiece(capturePos) - board.movePiece(from, to) - board.Rule50 = 0 - case Attack: - board.removePiece(to) - board.movePiece(from, to) - board.Rule50 = 0 - case DoublePawnPush: - board.EPSquare = uint8(int8(to) + EPDelta[board.ColorToMove]) - if (PawnAttacks[board.ColorToMove][board.EPSquare] & board.PieceBB[board.ColorToMove^1][Pawn]) == 0 { - board.EPSquare = NoEPSquare - } - fallthrough - case Quiet: - board.movePiece(from, to) - - if state.Moved.Type == Pawn { - board.Rule50 = 0 - } - } - - board.CastlingRights.ClearBit(from) - board.CastlingRights.ClearBit(to) - board.hashCastlingRights(state) - - if board.EPSquare != NoEPSquare { - board.Hash ^= Random64[EPRand64+uint16(FileOf(board.EPSquare))] - } - - board.KingPos[board.ColorToMove][state.Moved.Type] = to - board.ColorToMove ^= 1 - board.Hash ^= Random64[SideToMoveRand64] - - board.RepitionPly++ - board.Repitions[board.RepitionPly] = board.Hash -} - -func (board *Board) UndoMove(move Move) { - board.historyPly-- - state := &board.history[board.historyPly] - - board.hashCastlingRights(state) - if board.EPSquare != NoEPSquare { - board.Hash ^= Random64[EPRand64+uint16(FileOf(board.EPSquare))] - } - - board.CastlingRights = state.CastlingRights - board.Rule50 = state.Rule50 - board.EPSquare = state.EPSquare - - board.ColorToMove ^= 1 - board.Hash ^= Random64[SideToMoveRand64] - - from, to, movType := move.FromSq(), move.ToSq(), move.MoveType() - - board.GamePly-- - - switch movType { - case CastleWKS: - board.movePiece(G1, E1) - board.movePiece(F1, H1) - case CastleWQS: - board.movePiece(C1, E1) - board.movePiece(D1, A1) - case CastleBKS: - board.movePiece(G8, E8) - board.movePiece(F8, H8) - case CastleBQS: - board.movePiece(C8, E8) - board.movePiece(D8, A8) - case KnightPromotion: - fallthrough - case BishopPromotion: - fallthrough - case RookPromotion: - fallthrough - case QueenPromotion: - board.removePiece(to) - if state.Captured.Type != NoType { - board.putPiece(state.Captured.Type, state.Captured.Color, to) - } - board.putPiece(Pawn, board.ColorToMove, from) - case AttackEP: - capturePos := uint8(int8(to) + EPDelta[board.ColorToMove]) - board.movePiece(to, from) - board.putPiece(Pawn, state.Captured.Color, capturePos) - case Attack: - board.removePiece(to) - board.putPiece(state.Captured.Type, state.Captured.Color, to) - board.putPiece(state.Moved.Type, board.ColorToMove, from) - case DoublePawnPush: - fallthrough - case Quiet: - board.movePiece(to, from) - } - - if board.EPSquare != NoEPSquare { - board.Hash ^= Random64[EPRand64+uint16(FileOf(board.EPSquare))] - } - - board.KingPos[board.ColorToMove][state.Moved.Type] = from - board.RepitionPly-- -} - -// Move a piece from the given square to the given square. -// For this function, the move is guaranteed to be quiet. -func (board *Board) movePiece(from, to uint8) { - piece := &board.Squares[from] - board.PieceBB[piece.Color][piece.Type].ClearBit(from) - board.SideBB[piece.Color].ClearBit(from) - board.Hash ^= getPieceHash(piece.Type, piece.Color, from) - - board.PieceBB[piece.Color][piece.Type].SetBit(to) - board.SideBB[piece.Color].SetBit(to) - board.Hash ^= getPieceHash(piece.Type, piece.Color, to) - - board.Squares[to].Type = piece.Type - board.Squares[to].Color = piece.Color - piece.Type = NoType - piece.Color = NoColor -} - -// Put the piece given on the given square -func (board *Board) putPiece(pieceType, pieceColor, to uint8) { - board.PieceBB[pieceColor][pieceType].SetBit(to) - board.SideBB[pieceColor].SetBit(to) - board.Squares[to].Type = pieceType - board.Squares[to].Color = pieceColor - board.Hash ^= getPieceHash(pieceType, pieceColor, to) -} - -// Remove the piece given on the given square. -func (board *Board) removePiece(from uint8) { - piece := &board.Squares[from] - board.PieceBB[piece.Color][piece.Type].ClearBit(from) - board.SideBB[piece.Color].ClearBit(from) - board.Hash ^= getPieceHash(piece.Type, piece.Color, from) - - piece.Type = NoType - piece.Color = NoColor -} - -// Test whether or not the king is attacked for the side -// who moved. Called after board.DoMove. -func (board *Board) KingIsAttacked(kingColor uint8) bool { - return sqIsAttacked(board, kingColor, board.KingPos[kingColor][King]) -} - -// Given a board square, return it's file. -func FileOf(sq uint8) uint8 { - return sq % 8 -} - -// Given a board square, return it's rank. -func RankOf(sq uint8) uint8 { - return sq / 8 -} - -// Convert a string board coordinate to its position -// number. -func CoordinateToPos(coordinate string) uint8 { - file := coordinate[0] - 'a' - rank := int(coordinate[1]-'0') - 1 - return uint8(rank*8 + int(file)) -} - -// Convert a position number to a string board coordinate. -func PosToCoordinate(pos uint8) string { - file := FileOf(pos) - rank := RankOf(pos) - return string(rune('a'+file)) + string(rune('0'+rank+1)) + return SouthDelta } diff --git a/engine/evaluation.go b/engine/evaluation.go new file mode 100644 index 0000000..a46b418 --- /dev/null +++ b/engine/evaluation.go @@ -0,0 +1,228 @@ +package engine + +const ( + // Constants which map a piece to how much weight it should have on the phase of the game. + PawnPhase int16 = 0 + KnightPhase int16 = 1 + BishopPhase int16 = 1 + RookPhase int16 = 2 + QueenPhase int16 = 4 + TotalPhase int16 = PawnPhase*16 + KnightPhase*4 + BishopPhase*4 + RookPhase*4 + QueenPhase*2 + + // Constants representing a draw or infinite (checkmate) value. + Inf int16 = 10000 + Draw int16 = -25 +) + +var PhaseValues [6]int16 = [6]int16{ + PawnPhase, + KnightPhase, + BishopPhase, + RookPhase, + QueenPhase, +} + +// Endgame and middlegame piece square tables, with piece values builtin. +// +// https://www.chessprogramming.org/Piece-Square_Tables +// +// Many thanks to Thomas Jahn for the tables, which can be found here: +// +// https://github.com/lithander/MinimalChessEngine/blob/master/MinimalChess/Evaluation.cs#L88 +// +var PSQT_MG [6][64]int16 = [6][64]int16{ + + // Piece-square table for pawns + { + 100, 100, 100, 100, 100, 100, 100, 100, + 176, 214, 147, 194, 189, 214, 132, 77, + 82, 88, 106, 113, 150, 146, 110, 73, + 67, 93, 83, 95, 97, 92, 99, 63, + 55, 74, 80, 89, 94, 86, 90, 55, + 55, 70, 68, 69, 76, 81, 101, 66, + 52, 84, 66, 60, 69, 99, 117, 60, + 100, 100, 100, 100, 100, 100, 100, 100, + }, + + // Piece-square table for knights + { + 116, 228, 271, 270, 338, 213, 278, 191, + 225, 247, 353, 331, 321, 360, 300, 281, + 258, 354, 343, 362, 389, 428, 375, 347, + 300, 332, 325, 360, 349, 379, 339, 333, + 298, 322, 325, 321, 337, 332, 332, 303, + 287, 297, 316, 319, 327, 320, 327, 294, + 276, 259, 300, 304, 308, 322, 296, 292, + 208, 290, 257, 274, 296, 284, 293, 284, + }, + + // Piece-square table for bishops + { + 292, 338, 254, 283, 299, 294, 337, 323, + 316, 342, 319, 319, 360, 385, 343, 295, + 342, 377, 373, 374, 368, 392, 385, 363, + 332, 338, 356, 384, 370, 380, 337, 341, + 327, 354, 353, 366, 373, 346, 345, 341, + 335, 350, 351, 347, 352, 361, 350, 344, + 333, 354, 354, 339, 344, 353, 367, 333, + 309, 341, 342, 325, 334, 332, 302, 313, + }, + + // Piece square table for rook + { + 493, 511, 487, 515, 514, 483, 485, 495, + 493, 498, 529, 534, 546, 544, 483, 508, + 465, 490, 499, 497, 483, 519, 531, 480, + 448, 464, 476, 495, 484, 506, 467, 455, + 442, 451, 468, 470, 476, 472, 498, 454, + 441, 461, 468, 465, 478, 481, 478, 452, + 443, 472, 467, 476, 483, 500, 487, 423, + 459, 463, 470, 479, 480, 480, 446, 458, + }, + + // Piece square table for queens + { + 865, 902, 922, 911, 964, 948, 933, 928, + 886, 865, 903, 921, 888, 951, 923, 940, + 902, 901, 907, 919, 936, 978, 965, 966, + 881, 885, 897, 894, 898, 929, 906, 915, + 907, 884, 899, 896, 904, 906, 912, 911, + 895, 916, 900, 902, 904, 912, 924, 917, + 874, 899, 918, 908, 915, 924, 911, 906, + 906, 899, 906, 918, 898, 890, 878, 858, + }, + + // Piece square table for kings + { + -11, 70, 55, 31, -37, -16, 22, 22, + 37, 24, 25, 36, 16, 8, -12, -31, + 33, 26, 42, 11, 11, 40, 35, -2, + 0, -9, 1, -21, -20, -22, -15, -60, + -25, 16, -27, -67, -81, -58, -40, -62, + 7, -2, -37, -77, -79, -60, -23, -26, + 12, 15, -13, -72, -56, -28, 15, 17, + -6, 44, 29, -58, 8, -25, 34, 28, + }, +} + +var PSQT_EG [6][64]int16 = [6][64]int16{ + + // Piece-square table for pawns + { + 100, 100, 100, 100, 100, 100, 100, 100, + 277, 270, 252, 229, 240, 233, 264, 285, + 190, 197, 182, 168, 155, 150, 180, 181, + 128, 117, 108, 102, 93, 100, 110, 110, + 107, 101, 89, 85, 86, 83, 92, 91, + 96, 96, 85, 92, 88, 83, 85, 82, + 107, 99, 97, 97, 100, 89, 89, 84, + 100, 100, 100, 100, 100, 100, 100, 100, + }, + + // Piece-square table for knights + { + 229, 236, 269, 250, 257, 249, 219, 188, + 252, 274, 263, 281, 273, 258, 260, 229, + 253, 264, 290, 289, 278, 275, 263, 243, + 267, 280, 299, 301, 299, 293, 285, 264, + 263, 273, 293, 301, 296, 293, 284, 261, + 258, 276, 278, 290, 287, 274, 260, 255, + 241, 259, 270, 277, 276, 262, 260, 237, + 253, 233, 258, 264, 261, 260, 234, 215, + }, + + // Piece-square table for bishops + { + 288, 278, 287, 292, 293, 290, 287, 277, + 289, 294, 301, 288, 296, 289, 294, 281, + 292, 289, 296, 292, 296, 300, 296, 293, + 293, 302, 305, 305, 306, 302, 296, 297, + 289, 293, 304, 308, 298, 301, 291, 288, + 285, 294, 304, 303, 306, 294, 290, 280, + 285, 284, 291, 299, 300, 290, 284, 271, + 277, 292, 286, 295, 294, 288, 290, 285, + }, + + // Piece square table for rook + { + 506, 500, 508, 502, 504, 507, 505, 503, + 505, 506, 502, 502, 491, 497, 506, 501, + 504, 503, 499, 500, 500, 495, 496, 496, + 503, 502, 510, 500, 502, 504, 500, 505, + 505, 509, 509, 506, 504, 503, 496, 495, + 500, 503, 500, 505, 498, 498, 499, 489, + 496, 495, 502, 505, 498, 498, 491, 499, + 492, 497, 498, 496, 493, 493, 497, 480, + }, + + // Piece square table for queens + { + 918, 937, 943, 945, 934, 926, 924, 942, + 907, 945, 946, 951, 982, 933, 928, 912, + 896, 921, 926, 967, 963, 937, 924, 915, + 926, 944, 939, 962, 983, 957, 981, 950, + 893, 949, 942, 970, 952, 956, 953, 936, + 911, 892, 933, 928, 934, 942, 934, 924, + 907, 898, 883, 903, 903, 893, 886, 888, + 886, 887, 890, 872, 916, 890, 906, 879, + }, + + // Piece square table for kings + { + -74, -43, -23, -25, -11, 10, 1, -12, + -18, 6, 4, 9, 7, 26, 14, 8, + -3, 6, 10, 6, 8, 24, 27, 3, + -16, 8, 13, 20, 14, 19, 10, -3, + -25, -14, 13, 20, 24, 15, 1, -15, + -27, -10, 9, 20, 23, 14, 2, -12, + -32, -17, 4, 14, 15, 5, -10, -22, + -55, -40, -23, -6, -20, -8, -28, -47, + }, +} + +// Flip white's perspective to black +var FlipSq [2][64]int = [2][64]int{ + { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63, + }, + + { + 56, 57, 58, 59, 60, 61, 62, 63, + 48, 49, 50, 51, 52, 53, 54, 55, + 40, 41, 42, 43, 44, 45, 46, 47, + 32, 33, 34, 35, 36, 37, 38, 39, + 24, 25, 26, 27, 28, 29, 30, 31, + 16, 17, 18, 19, 20, 21, 22, 23, + 8, 9, 10, 11, 12, 13, 14, 15, + 0, 1, 2, 3, 4, 5, 6, 7, + }, +} + +// Evaluate a board and give a score, from the perspective of the side to move ( +// more positive if it's good for the side to move, otherwise more negative). +func evaluatePos(pos *Position) int16 { + var mgScores, egScores [2]int16 + phase := TotalPhase + + usBB := pos.SideBB[pos.SideToMove] | pos.SideBB[pos.SideToMove^1] + for usBB != 0 { + sq := usBB.PopBit() + piece := pos.Squares[sq] + + mgScores[piece.Color] += PSQT_MG[piece.Type][FlipSq[piece.Color][sq]] + egScores[piece.Color] += PSQT_EG[piece.Type][FlipSq[piece.Color][sq]] + phase -= PhaseValues[piece.Type] + } + + mgScore := mgScores[pos.SideToMove] - mgScores[pos.SideToMove^1] + egScore := egScores[pos.SideToMove] - egScores[pos.SideToMove^1] + phase = (phase*256 + (TotalPhase / 2)) / TotalPhase + return int16(((int32(mgScore) * (int32(256) - int32(phase))) + (int32(egScore) * int32(phase))) / int32(256)) +} diff --git a/engine/move.go b/engine/move.go index 4c8f133..f097a00 100644 --- a/engine/move.go +++ b/engine/move.go @@ -2,126 +2,135 @@ package engine import "fmt" -// A file containg utility methods and type definitions -// for moves in Blunder. +// move.go constaints the implementation of a move datatype. const ( - Quiet uint8 = iota - DoublePawnPush - Attack - AttackEP - KnightPromotion - BishopPromotion - RookPromotion - QueenPromotion - CastleWKS - CastleWQS - CastleBKS - CastleBQS - - MaxMoves = 50 -) + // Constants represeting the four possible move types. + Quiet uint8 = 0 + Attack uint8 = 1 + Castle uint8 = 2 + Promotion uint8 = 3 -type Move uint16 -type Moves = []Move + // Constants representing move flags indicating what kind of promotion + // is occuring. + KnightPromotion uint8 = 0 + BishopPromotion uint8 = 1 + RookPromotion uint8 = 2 + QueenPromotion uint8 = 3 -// A helper function to create moves. Each move generated -// by our move generator is encoded in 16-bits, where the -// first six bits are the from square, the second 6, are the -// to square, and the last four are the move type (see above). -func MakeMove(from, to, moveType uint8) Move { - return Move(uint16(from)<<10 | uint16(to)<<4 | uint16(moveType)) -} + // A constant representing a move flag indicating an attack is an en passant + // attack. + AttackEP uint8 = 1 -// Get the type of the move. -func (move Move) MoveType() uint8 { - return uint8(move & 0xf) + // A constant representing a null flag + NoFlag uint8 = 0 +) + +type Move uint32 + +// Create a new move. The first 6 bits are the from square, the next 6 bits are the to square, +// the next two represent the move type, the next two are reserved for any speical flags needed +// to give full information concering the move, and the last 16-bits are used for scoring a move +// for move-ordering in the search phase. +func NewMove(from, to, moveType, flag uint8) Move { + return Move(uint32(from)<<26 | uint32(to)<<20 | uint32(moveType)<<18 | uint32(flag)<<16) } // Get the from square of the move. func (move Move) FromSq() uint8 { - return uint8((move & 0xFC00) >> 10) + return uint8((move & 0xfc000000) >> 26) } // Get the to square of the move. func (move Move) ToSq() uint8 { - return uint8((move & 0x3F0) >> 4) + return uint8((move & 0x3f00000) >> 20) +} + +// Get the type of the move. +func (move Move) MoveType() uint8 { + return uint8((move & 0xc0000) >> 18) +} + +// Get the flag of the move. +func (move Move) Flag() uint8 { + return uint8((move & 0x30000) >> 16) +} + +// Get the score of a move. +func (move Move) Score() uint8 { + return uint8(move & 0xffff) +} + +// Add a score to the move for move ordering. +func (move *Move) AddScore(score uint8) { + (*move) |= Move(score) +} + +// Test if two moves are equal. +func (move Move) Equal(m2 Move) bool { + return (move & 0xffff0000) == (m2 & 0xffff0000) } // A helper function to extract the info from a move represented // as 32-bits, and display it. func (move Move) String() string { - from, to, movType := move.FromSq(), move.ToSq(), move.MoveType() - promotionType, seperator := "", "-" - switch movType { - case Attack: - fallthrough - case AttackEP: - seperator = "x" - case KnightPromotion: - promotionType = "n" - case BishopPromotion: - promotionType = "b" - case RookPromotion: - promotionType = "r" - case QueenPromotion: - promotionType = "q" + from, to, moveType, flag := move.FromSq(), move.ToSq(), move.MoveType(), move.Flag() + + promotionType := "" + if moveType == Promotion { + switch flag { + case KnightPromotion: + promotionType = "n" + case BishopPromotion: + promotionType = "b" + case RookPromotion: + promotionType = "r" + case QueenPromotion: + promotionType = "q" + } } - return fmt.Sprintf( - "%v%v%v%v", - PosToCoordinate(from), - seperator, - PosToCoordinate(to), - promotionType, - ) + return fmt.Sprintf("%v%v%v", posToCoordinate(from), posToCoordinate(to), promotionType) } -func MoveFromCoord(board *Board, move string, useChess960Castling bool) Move { - fromPos := CoordinateToPos(move[0:2]) - toPos := CoordinateToPos(move[2:4]) - movePieceType := board.Squares[fromPos].Type +// Convert a move in UCI format into a Move +func moveFromCoord(pos *Position, move string) Move { + from := coordinateToPos(move[0:2]) + to := coordinateToPos(move[2:4]) + moved := pos.Squares[from].Type + var moveType uint8 + flag := NoFlag moveLen := len(move) if moveLen == 5 { + moveType = Promotion if move[moveLen-1] == 'n' { - moveType = KnightPromotion + flag = KnightPromotion } else if move[moveLen-1] == 'b' { - moveType = BishopPromotion + flag = BishopPromotion } else if move[moveLen-1] == 'r' { - moveType = RookPromotion + flag = RookPromotion } else if move[moveLen-1] == 'q' { - moveType = QueenPromotion + flag = QueenPromotion } - } else if move == "e1g1" && movePieceType == King && !useChess960Castling { - moveType = CastleWKS - } else if move == "e1c1" && movePieceType == King && !useChess960Castling { - moveType = CastleWQS - } else if move == "e8g8" && movePieceType == King && !useChess960Castling { - moveType = CastleBKS - } else if move == "e8c8" && movePieceType == King && !useChess960Castling { - moveType = CastleBQS - } else if move == "e1h1" && movePieceType == King && useChess960Castling { - moveType = CastleWKS - } else if move == "e1a1" && movePieceType == King && useChess960Castling { - moveType = CastleWQS - } else if move == "e8h8" && movePieceType == King && useChess960Castling { - moveType = CastleBKS - } else if move == "e8a8" && movePieceType == King && useChess960Castling { - moveType = CastleBQS - } else if toPos == board.EPSquare && movePieceType == Pawn { - moveType = AttackEP + } else if move == "e1g1" && moved == King { + moveType = Castle + } else if move == "e1c1" && moved == King { + moveType = Castle + } else if move == "e8g8" && moved == King { + moveType = Castle + } else if move == "e8c8" && moved == King { + moveType = Castle + } else if to == pos.EPSq && moved == Pawn { + moveType = Attack + flag = AttackEP } else { - capturePieceType := board.Squares[toPos].Type - if capturePieceType == NoType { - if movePieceType == Pawn && abs(int8(fromPos)-int8(toPos)) == 16 { - moveType = DoublePawnPush - } else { - moveType = Quiet - } + captured := pos.Squares[to] + if captured.Type == NoType { + moveType = Quiet } else { moveType = Attack } } - return MakeMove(fromPos, toPos, moveType) + return NewMove(from, to, moveType, flag) } diff --git a/engine/movegen.go b/engine/movegen.go index a127e6d..b5e3f59 100644 --- a/engine/movegen.go +++ b/engine/movegen.go @@ -1,106 +1,65 @@ package engine -import "fmt" +// movegen.go implements the move generator for Blunder. -// A file containg the move generator of Blunder +import ( + "fmt" +) const ( // These masks help determine whether or not the squares between // the king and it's rooks are clear for castling F1_G1, B1_C1_D1 = 0x600000000000000, 0x7000000000000000 F8_G8, B8_C8_D8 = 0x6, 0x70 - FullBB = 0xffffffffffffffff ) // Generate all pseduo-legal moves for a given position. -func GenPseduoLegalMoves(board *Board) (moves Moves) { - kingPos := board.KingPos[board.ColorToMove][King] - checkers := attackersOfSquare(board, board.ColorToMove, kingPos) - var targets Bitboard = FullBB - - if checkers.CountBits() > 1 { - genPieceMoves(board, King, kingPos, &moves, targets, false) - return - } else if checkers.CountBits() == 1 { - checkerPos := checkers.Msb() - if board.Squares[checkerPos].Type == Knight { - targets = SquareBB[checkerPos] - } else { - targets = LinesBewteen[kingPos][checkerPos] - } - } - - moves = make([]Move, 0, 100) +func genMoves(pos *Position) (moves MoveList) { + // Go through each piece type, and each piece for that type, + // and generate the moves for that piece. var piece uint8 - for piece = Knight; piece < NoType; piece++ { - piecesBB := board.PieceBB[board.ColorToMove][piece] + piecesBB := pos.PieceBB[pos.SideToMove][piece] for piecesBB != 0 { - piecePos := piecesBB.PopBit() - genPieceMoves(board, piece, piecePos, &moves, targets, false) + pieceSq := piecesBB.PopBit() + genPieceMoves(pos, piece, pieceSq, &moves) } } - genPawnMoves(board, &moves, targets) - genCastlingMoves(board, &moves) - return moves -} + // Generate pawn moves. + genPawnMoves(pos, &moves) -// Generate all pseduo-legal moves for a given position. -func GenPseduoLegalCaptures(board *Board) (moves Moves) { - kingPos := board.KingPos[board.ColorToMove][King] - checkers := attackersOfSquare(board, board.ColorToMove, kingPos) - var targets = board.SideBB[board.ColorToMove^1] - - if checkers.CountBits() > 1 { - genPieceMoves(board, King, kingPos, &moves, targets, true) - return - } else if checkers.CountBits() == 1 { - checkerPos := checkers.Msb() - targets = SquareBB[checkerPos] - } - - moves = make([]Move, 0, 100) - var piece uint8 + // Generate castling moves. + genCastlingMoves(pos, &moves) - for piece = Knight; piece < NoType; piece++ { - piecesBB := board.PieceBB[board.ColorToMove][piece] - for piecesBB != 0 { - piecePos := piecesBB.PopBit() - genPieceMoves(board, piece, piecePos, &moves, targets, true) - } - } - genPawnMoves(board, &moves, targets) return moves } -// Generate the moves a single piece, making sure they all align with the squares specified -// by a targets bitboard. Filter king moves using the target bitboard if includeKing is set to -// true. -func genPieceMoves(board *Board, piece, sq uint8, moves *Moves, targets Bitboard, includeKing bool) { - usBB := board.SideBB[board.ColorToMove] - enemyBB := board.SideBB[board.ColorToMove^1] +// Generate the moves a single piece, +func genPieceMoves(pos *Position, piece, sq uint8, moves *MoveList) { + // Get a bitboard representing our side and the enemy side. + usBB := pos.SideBB[pos.SideToMove] + enemyBB := pos.SideBB[pos.SideToMove^1] + // Figure out what type of piece we're dealing with, and + // generate the moves it has accordingly. switch piece { case Knight: - knightMoves := (KnightMoves[sq] & ^usBB) & targets - genMovesFromBB(board, sq, knightMoves, enemyBB, moves) + knightMoves := KnightMoves[sq] & ^usBB + genMovesFromBB(pos, sq, knightMoves, enemyBB, moves) case King: kingMoves := KingMoves[sq] & ^usBB - if includeKing { - kingMoves &= targets - } - genMovesFromBB(board, sq, kingMoves, enemyBB, moves) + genMovesFromBB(pos, sq, kingMoves, enemyBB, moves) case Bishop: - bishopMoves := (genBishopMoves(sq, usBB|enemyBB) & ^usBB) & targets - genMovesFromBB(board, sq, bishopMoves, enemyBB, moves) + bishopMoves := genBishopMoves(sq, usBB|enemyBB) & ^usBB + genMovesFromBB(pos, sq, bishopMoves, enemyBB, moves) case Rook: - rookMoves := (genRookMoves(sq, usBB|enemyBB) & ^usBB) & targets - genMovesFromBB(board, sq, rookMoves, enemyBB, moves) + rookMoves := genRookMoves(sq, usBB|enemyBB) & ^usBB + genMovesFromBB(pos, sq, rookMoves, enemyBB, moves) case Queen: - bishopMoves := (genBishopMoves(sq, usBB|enemyBB) & ^usBB) & targets - rookMoves := (genRookMoves(sq, usBB|enemyBB) & ^usBB) & targets - genMovesFromBB(board, sq, bishopMoves|rookMoves, enemyBB, moves) + bishopMoves := genBishopMoves(sq, usBB|enemyBB) & ^usBB + rookMoves := genRookMoves(sq, usBB|enemyBB) & ^usBB + genMovesFromBB(pos, sq, bishopMoves|rookMoves, enemyBB, moves) } } @@ -121,59 +80,58 @@ func genBishopMoves(sq uint8, blockers Bitboard) Bitboard { // Generate pawn moves for the current side. Pawns are treated // seperately from the rest of the pieces as they have more // complicated and exceptional rules for how they can move. -func genPawnMoves(board *Board, moves *Moves, targets Bitboard) { - usBB := board.SideBB[board.ColorToMove] - enemyBB := board.SideBB[board.ColorToMove^1] - pawnsBB := board.PieceBB[board.ColorToMove][Pawn] - +// Only generate the moves that align with the specified +// target squares. +func genPawnMoves(pos *Position, moves *MoveList) { + usBB := pos.SideBB[pos.SideToMove] + enemyBB := pos.SideBB[pos.SideToMove^1] + pawnsBB := pos.PieceBB[pos.SideToMove][Pawn] + + // For each pawn on our side... for pawnsBB != 0 { from := pawnsBB.PopBit() - pawnOnePush := PawnPushes[board.ColorToMove][from] & ^(usBB | enemyBB) + pawnOnePush := PawnPushes[pos.SideToMove][from] & ^(usBB | enemyBB) pawnTwoPush := ((pawnOnePush & MaskRank[Rank6]) << 8) & ^(usBB | enemyBB) - if board.ColorToMove == White { + if pos.SideToMove == White { pawnTwoPush = ((pawnOnePush & MaskRank[Rank3]) >> 8) & ^(usBB | enemyBB) } - pawnPush := (pawnOnePush | pawnTwoPush) & targets - pawnAttacks := PawnAttacks[board.ColorToMove][from] & (targets | SquareBB[board.EPSquare]) + // calculate the push move for the pawn... + pawnPush := pawnOnePush | pawnTwoPush + + // and the attacks. + pawnAttacks := PawnAttacks[pos.SideToMove][from] + + // Generate pawn push moves for pawnPush != 0 { to := pawnPush.PopBit() - if isPromoting(board.ColorToMove, to) { - makePromotionMoves(board, from, to, moves) + if isPromoting(pos.SideToMove, to) { + makePromotionMoves(pos, from, to, moves) continue } - if abs(int8(from)-int8(to)) == 16 { - *moves = append(*moves, MakeMove(from, to, DoublePawnPush)) - continue - } - *moves = append(*moves, MakeMove(from, to, Quiet)) + moves.AddMove(NewMove(from, to, Quiet, NoFlag)) } + + // Generate pawn attack moves. for pawnAttacks != 0 { to := pawnAttacks.PopBit() toBB := SquareBB[to] - if to == board.EPSquare { - *moves = append(*moves, MakeMove(from, to, AttackEP)) + // Check for en passant moves. + if to == pos.EPSq { + moves.AddMove(NewMove(from, to, Attack, AttackEP)) } else if toBB&enemyBB != 0 { - if isPromoting(board.ColorToMove, to) { - makePromotionMoves(board, from, to, moves) + if isPromoting(pos.SideToMove, to) { + makePromotionMoves(pos, from, to, moves) continue } - *moves = append(*moves, MakeMove(from, to, Attack)) + moves.AddMove(NewMove(from, to, Attack, NoFlag)) } } } } -// Get the absolute value of a number n -func abs(n int8) int8 { - if n < 0 { - return -n - } - return n -} - // A helper function to determine if a pawn has reached the 8th or // 1st rank and will promote. func isPromoting(usColor, toSq uint8) bool { @@ -184,42 +142,42 @@ func isPromoting(usColor, toSq uint8) bool { } // Generate promotion moves for pawns -func makePromotionMoves(board *Board, from, to uint8, moves *Moves) { - *moves = append(*moves, MakeMove(from, to, KnightPromotion)) - *moves = append(*moves, MakeMove(from, to, BishopPromotion)) - *moves = append(*moves, MakeMove(from, to, RookPromotion)) - *moves = append(*moves, MakeMove(from, to, QueenPromotion)) +func makePromotionMoves(pos *Position, from, to uint8, moves *MoveList) { + moves.AddMove(NewMove(from, to, Promotion, KnightPromotion)) + moves.AddMove(NewMove(from, to, Promotion, BishopPromotion)) + moves.AddMove(NewMove(from, to, Promotion, RookPromotion)) + moves.AddMove(NewMove(from, to, Promotion, QueenPromotion)) } // Generate castling moves. Note testing whether or not castling has the king // crossing attacked squares is not tested for here, as pseduo-legal move // generation is the focus. -func genCastlingMoves(board *Board, moves *Moves) { - allPieces := board.SideBB[board.ColorToMove] | board.SideBB[board.ColorToMove^1] - if board.ColorToMove == White { - if board.CastlingRights&WhiteKingside == WhiteKingside && (allPieces&F1_G1) == 0 && (!sqIsAttacked(board, board.ColorToMove, E1) && - !sqIsAttacked(board, board.ColorToMove, F1) && !sqIsAttacked(board, board.ColorToMove, G1)) { - *moves = append(*moves, MakeMove(E1, G1, CastleWKS)) +func genCastlingMoves(pos *Position, moves *MoveList) { + allPieces := pos.SideBB[pos.SideToMove] | pos.SideBB[pos.SideToMove^1] + if pos.SideToMove == White { + if pos.CastlingRights&WhiteKingsideRight != 0 && (allPieces&F1_G1) == 0 && (!sqIsAttacked(pos, pos.SideToMove, E1) && + !sqIsAttacked(pos, pos.SideToMove, F1) && !sqIsAttacked(pos, pos.SideToMove, G1)) { + moves.AddMove(NewMove(E1, G1, Castle, NoFlag)) } - if board.CastlingRights&WhiteQueenside == WhiteQueenside && (allPieces&B1_C1_D1) == 0 && (!sqIsAttacked(board, board.ColorToMove, E1) && - !sqIsAttacked(board, board.ColorToMove, D1) && !sqIsAttacked(board, board.ColorToMove, C1)) { - *moves = append(*moves, MakeMove(E1, C1, CastleWQS)) + if pos.CastlingRights&WhiteQueensideRight != 0 && (allPieces&B1_C1_D1) == 0 && (!sqIsAttacked(pos, pos.SideToMove, E1) && + !sqIsAttacked(pos, pos.SideToMove, D1) && !sqIsAttacked(pos, pos.SideToMove, C1)) { + moves.AddMove(NewMove(E1, C1, Castle, NoFlag)) } } else { - if board.CastlingRights&BlackKingside == BlackKingside && (allPieces&F8_G8) == 0 && (!sqIsAttacked(board, board.ColorToMove, E8) && - !sqIsAttacked(board, board.ColorToMove, F8) && !sqIsAttacked(board, board.ColorToMove, G8)) { - *moves = append(*moves, MakeMove(E8, G8, CastleBKS)) + if pos.CastlingRights&BlackKingsideRight != 0 && (allPieces&F8_G8) == 0 && (!sqIsAttacked(pos, pos.SideToMove, E8) && + !sqIsAttacked(pos, pos.SideToMove, F8) && !sqIsAttacked(pos, pos.SideToMove, G8)) { + moves.AddMove(NewMove(E8, G8, Castle, NoFlag)) } - if board.CastlingRights&BlackQueenside == BlackQueenside && (allPieces&B8_C8_D8) == 0 && (!sqIsAttacked(board, board.ColorToMove, E8) && - !sqIsAttacked(board, board.ColorToMove, D8) && !sqIsAttacked(board, board.ColorToMove, C8)) { - *moves = append(*moves, MakeMove(E8, C8, CastleBQS)) + if pos.CastlingRights&BlackQueensideRight != 0 && (allPieces&B8_C8_D8) == 0 && (!sqIsAttacked(pos, pos.SideToMove, E8) && + !sqIsAttacked(pos, pos.SideToMove, D8) && !sqIsAttacked(pos, pos.SideToMove, C8)) { + moves.AddMove(NewMove(E8, C8, Castle, NoFlag)) } } } // From a bitboard representing possible squares a piece can move, // serialize it, and generate a list of moves. -func genMovesFromBB(board *Board, from uint8, movesBB, enemyBB Bitboard, moves *Moves) { +func genMovesFromBB(pos *Position, from uint8, movesBB, enemyBB Bitboard, moves *MoveList) { for movesBB != 0 { to := movesBB.PopBit() toBB := SquareBB[to] @@ -227,20 +185,27 @@ func genMovesFromBB(board *Board, from uint8, movesBB, enemyBB Bitboard, moves * if toBB&enemyBB != 0 { moveType = Attack } - *moves = append(*moves, MakeMove(from, to, moveType)) + moves.AddMove(NewMove(from, to, moveType, NoFlag)) } } -func sqIsAttacked(board *Board, usColor, sq uint8) bool { - enemyBB := board.SideBB[usColor^1] - usBB := board.SideBB[usColor] +// Given a side and a square, test if the square for the given side +// is under attack by the enemy side. +func sqIsAttacked(pos *Position, usColor, sq uint8) bool { + // The algorithm used here is to pretend to place a "superpiece" - a piece that + // can move like a queen and knight - on our square of interest. Rays are then sent + // out from this superpiece sitting on our square, and if any of these rays hit + // an enemy piece, we know our square is being attacked by an enemy piece. + + enemyBB := pos.SideBB[usColor^1] + usBB := pos.SideBB[usColor] - enemyBishops := board.PieceBB[usColor^1][Bishop] - enemyRooks := board.PieceBB[usColor^1][Rook] - enemyQueens := board.PieceBB[usColor^1][Queen] - enemyKnights := board.PieceBB[usColor^1][Knight] - enemyKing := board.PieceBB[usColor^1][King] - enemyPawns := board.PieceBB[usColor^1][Pawn] + enemyBishops := pos.PieceBB[usColor^1][Bishop] + enemyRooks := pos.PieceBB[usColor^1][Rook] + enemyQueens := pos.PieceBB[usColor^1][Queen] + enemyKnights := pos.PieceBB[usColor^1][Knight] + enemyKing := pos.PieceBB[usColor^1][King] + enemyPawns := pos.PieceBB[usColor^1][Pawn] intercardinalRays := genBishopMoves(sq, enemyBB|usBB) cardinalRaysRays := genRookMoves(sq, enemyBB|usBB) @@ -264,72 +229,63 @@ func sqIsAttacked(board *Board, usColor, sq uint8) bool { return false } -// Compute a bitboard representing the enemy attackers of a particular square. -func attackersOfSquare(board *Board, usColor, sq uint8) (attackers Bitboard) { - enemyBB := board.SideBB[usColor^1] - usBB := board.SideBB[usColor] - - enemyBishops := board.PieceBB[usColor^1][Bishop] - enemyRooks := board.PieceBB[usColor^1][Rook] - enemyQueens := board.PieceBB[usColor^1][Queen] - enemyKnights := board.PieceBB[usColor^1][Knight] - enemyKing := board.PieceBB[usColor^1][King] - enemyPawns := board.PieceBB[usColor^1][Pawn] - - intercardinalRays := genBishopMoves(sq, enemyBB|usBB) - cardinalRaysRays := genRookMoves(sq, enemyBB|usBB) - - attackers |= intercardinalRays & (enemyBishops | enemyQueens) - attackers |= cardinalRaysRays & (enemyRooks | enemyQueens) - attackers |= KnightMoves[sq] & enemyKnights - attackers |= KingMoves[sq] & enemyKing - attackers |= PawnAttacks[usColor][sq] & enemyPawns - return attackers -} - -const TTSize = 10000000 - -type perftTTEntry struct { - Hash uint64 - NodeCount uint64 - Depth int -} - -var TT [TTSize]perftTTEntry - // Explore the move tree up to depth, and return the total // number of nodes explored. This function is used to // debug move generation and ensure it is working by comparing // the results to the known results of other engines -func Perft(board *Board, depth, divdeAt int, silent bool) uint64 { +func DividePerft(pos *Position, depth, divdeAt uint8) uint64 { + // If depth zero has been reached, return zero... if depth == 0 { return 1 } - if entry := TT[board.Hash%TTSize]; entry.Hash == board.Hash && entry.Depth == depth { - return entry.NodeCount - } - - moves := GenPseduoLegalMoves(board) + // otherwise genrate the legal moves we have... + moves := genMoves(pos) var nodes uint64 - for _, move := range moves { - board.DoMove(move, true) - if board.KingIsAttacked(board.ColorToMove ^ 1) { - board.UndoMove(move) - continue + // And make every move, recursively calling perft to get the number of subnodes + // for each move. + var idx uint8 + for idx = 0; idx < moves.Count; idx++ { + move := moves.Moves[idx] + if pos.MakeMove(move) { + moveNodes := DividePerft(pos, depth-1, divdeAt) + if depth == divdeAt { + fmt.Printf("%v: %v\n", move, moveNodes) + } + + nodes += moveNodes } - moveNodes := Perft(board, depth-1, divdeAt, silent) + pos.UnmakeMove(move) + } - if depth == divdeAt && !silent { - fmt.Printf("%v: %v\n", move, moveNodes) - } + // Return the total amount of nodes for the given position. + return nodes +} - nodes += moveNodes - board.UndoMove(move) +// Same as divide perft but doesn't print subnode count +// for each move, only the final total. +func Perft(pos *Position, depth uint8) uint64 { + // If depth zero has been reached, return zero... + if depth == 0 { + return 1 + } + + // otherwise genrate the legal moves we have... + moves := genMoves(pos) + var nodes uint64 + + // And make every move, recursively calling perft to get the number of subnodes + // for each move. + var idx uint8 + for idx = 0; idx < moves.Count; idx++ { + if pos.MakeMove(moves.Moves[idx]) { + nodes += Perft(pos, depth-1) + } + pos.UnmakeMove(moves.Moves[idx]) } - TT[board.Hash%TTSize] = perftTTEntry{Hash: board.Hash, NodeCount: nodes, Depth: depth} + // Return the total amount of nodes for the given position. return nodes } diff --git a/tests/perfttest.go b/engine/movegen_test.go similarity index 87% rename from tests/perfttest.go rename to engine/movegen_test.go index 51be78e..95553c6 100644 --- a/tests/perfttest.go +++ b/engine/movegen_test.go @@ -1,13 +1,16 @@ -package tests +package engine + +// movegen_test.go implements a file parser to read in test positions to ensure +// Blunder's move generator is working correctly. import ( - "blunder/engine" "bufio" "fmt" "os" "path/filepath" "strconv" "strings" + "testing" "time" ) @@ -27,7 +30,7 @@ type PerftTest struct { func loadPerftSuite() (perftTests []PerftTest) { wd, _ := os.Getwd() parentFolder := filepath.Dir(wd) - filePath := filepath.Join(parentFolder, "/tests/perftsuite.txt") + filePath := filepath.Join(parentFolder, "/testdata/perftsuite.epd") file, err := os.Open(filePath) if err != nil { @@ -87,25 +90,27 @@ func printPerftTestRowSeparator() { } // Test blunder against the perft suite -func RunPerftTests(board *engine.Board) { +func TestMovegen(t *testing.T) { printPerftTestRowSeparator() printPerftTestRow("position", "depth", "expected", "moves", "correct") printPerftTestRowSeparator() - perftTests := loadPerftSuite() + var pos Position var totalNodes uint64 testsPassed := true + + perftTests := loadPerftSuite() start := time.Now() for _, perftTest := range perftTests { - board.LoadFEN(perftTest.FEN) + pos.LoadFEN(perftTest.FEN) for depth, nodeCount := range perftTest.DepthValues { if nodeCount == 0 { continue } - result := engine.Perft(board, depth+1, depth+1, true) + result := Perft(&pos, uint8(depth)+1) totalNodes += result var correct string @@ -127,10 +132,8 @@ func RunPerftTests(board *engine.Board) { } } - if testsPassed { - fmt.Println("\nAll tests passed") - } else { - fmt.Println("\nTesting failed on some positions") + if !testsPassed { + t.Error("\nTesting failed on some positions. See table for exact positions.") } fmt.Println("\nTotal Nodes:", totalNodes) diff --git a/engine/movelist.go b/engine/movelist.go new file mode 100644 index 0000000..3681c02 --- /dev/null +++ b/engine/movelist.go @@ -0,0 +1,15 @@ +package engine + +// movelist.go implements a very basic stack for holding moves + +const MaxMoves = 255 + +type MoveList struct { + Moves [MaxMoves]Move + Count uint8 +} + +func (moveList *MoveList) AddMove(move Move) { + moveList.Moves[moveList.Count] = move + moveList.Count++ +} diff --git a/engine/polyglot.go b/engine/polyglot.go deleted file mode 100644 index 5e7c5be..0000000 --- a/engine/polyglot.go +++ /dev/null @@ -1,125 +0,0 @@ -package engine - -import ( - "bufio" - "bytes" - "encoding/binary" - "fmt" - "io" - "os" -) - -// The functions in the file provide the engine with -// a polyglot book loader. This is used to verify -// zobrist hashing is working correctly, and use opening -// books in the engine. The specification which this parser -// uses can be found at: -// -// http://hgm.nubati.net/book_format.html - -const ( - // The size of a polyglot entry - EntryByteLength = 16 - // Each masks helps to extract the correct bits - // from the move part of a polyglot entry. - ToFileMask uint16 = 0x7 - ToRankMask uint16 = 0x38 - FromFileMask uint16 = 0x1C0 - FromRankMask uint16 = 0xE00 - PromotionPieceMask uint16 = 0x7000 - - // Each shift works together with a move - // mask above to extract the correct number - // from the move part of a polyglot entry - // by shifting the masked bits to the least - // significant end of a bitstring. - ToRankShift = 3 - FromFileShift = 6 - FromRankShift = 9 - PromotionPieceShift = 12 - - // Characters representing the eight files - // and ranks - fileCharacters = "abcdefgh" - rankCharacters = "12345678" -) - -// Each polyglot book is composed of a series of 16-byte entries. Each -// of these entries contains a key, which is the hash of the position -// after the current moves have been made, the moves made, the weight -// those moves are given (i.e. how good they are), and a learn field, -// which, as far as I can tell, is usually ignored and set to zero -// by polyglot book generators, so it's not included here. -type PolyglotEntry struct { - Hash uint64 - Move string - Weight uint16 -} - -// Parse a polyglot file and create a map of PolyglotEntry's -// from it. Each zobrist hash for an entry maps to the moves -// and weight of the entry. -func LoadPolyglotFile(path string) (map[uint64]PolyglotEntry, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - reader := bufio.NewReader(file) - entries := make(map[uint64]PolyglotEntry) - - for { - var entryBytes [EntryByteLength]byte - _, err := io.ReadFull(reader, entryBytes[:]) - if err == io.EOF { - break - } else if err != nil { - return nil, err - } - - var entry PolyglotEntry - - // Load Zobrist hash - bytesBuffer := bytes.NewBuffer(entryBytes[0:8]) - binary.Read(bytesBuffer, binary.BigEndian, &entry.Hash) - - // Load the move - var move uint16 - bytesBuffer.Reset() - bytesBuffer.Write(entryBytes[8:10]) - binary.Read(bytesBuffer, binary.BigEndian, &move) - - toFile := fileCharacters[move&ToFileMask] - toRank := rankCharacters[(move&ToRankMask)>>ToRankShift] - fromFile := fileCharacters[(move&FromFileMask)>>FromFileShift] - fromRank := rankCharacters[(move&FromRankMask)>>FromRankShift] - promotionPiece := (move & PromotionPieceMask) >> PromotionPieceShift - - promotionCharacter := "" - switch promotionPiece { - case 1: - promotionCharacter = "n" - case 2: - promotionCharacter = "b" - case 3: - promotionCharacter = "r" - case 4: - promotionCharacter = "q" - } - entry.Move = fmt.Sprintf("%c%c%c%c%v", fromFile, fromRank, toFile, toRank, promotionCharacter) - - // Load the weight - var weight uint16 - bytesBuffer.Reset() - bytesBuffer.Write(entryBytes[10:12]) - binary.Read(bytesBuffer, binary.BigEndian, &weight) - entry.Weight = weight - - // Load the learn data, and discard it - var learn uint32 - bytesBuffer.Reset() - bytesBuffer.Write(entryBytes[12:16]) - binary.Read(bytesBuffer, binary.BigEndian, &learn) - entries[entry.Hash] = entry - } - return entries, nil -} diff --git a/engine/search.go b/engine/search.go new file mode 100644 index 0000000..ccebd34 --- /dev/null +++ b/engine/search.go @@ -0,0 +1,456 @@ +package engine + +import ( + "fmt" + "time" +) + +// search.go implements the search routine for Blunder. + +const ( + // A constant representing no move. + NullMove Move = 0 + + // A constant representing the maximum search depth that + // will be attempted. + MaxDepth = 50 + + // The score the best move from the transposition table will + // be given. + TT_BestMoveScore = 200 + + // Scores for the two killers from each ply. They're ranked below the hash move, + // and good captures, but above normal quiet moves. + FirstKillerMoveScore = 10 + SecondKillerMoveScore = 9 +) + +// An array that maps move scores to attacker and victim piece types +// for MVV-LVA move ordering: https://www.chessprogramming.org/MVV-LVA. +var MvvLva [7][6]uint8 = [7][6]uint8{ + {16, 15, 14, 13, 12, 11}, // victim Pawn + {26, 25, 24, 23, 22, 21}, // victim Knight + {36, 35, 34, 33, 32, 31}, // victim Bishop + {46, 45, 44, 43, 42, 41}, // vitcim Rook + {56, 55, 54, 53, 52, 51}, // victim Queen + + {0, 0, 0, 0, 0, 0}, // victim King + {0, 0, 0, 0, 0, 0}, // No piece +} + +// A struct that holds state needed during the search phase. The search +// routines are thus implemented as methods of this struct. +type Search struct { + Pos Position + TransTable TransTable + Timer TimeManager + + killerMoves [MaxDepth][2]Move + nodesSearched uint64 + selectiveDepth uint8 + engineColor uint8 +} + +// The main search function for Blunder, implemented as an interative +// deepening loop. +func (search *Search) Search() Move { + search.engineColor = search.Pos.SideToMove + bestScore := -Inf + bestMove := NullMove + + search.Timer.Start() + + for depth := 1; depth <= MaxDepth; depth++ { + // Start a search, and time it for reporting purposes. + startTime := time.Now() + move, score := search.rootNegamax(uint8(depth)) + elapsedTime := time.Since(startTime) + + if search.Timer.Stop { + break + } + + // Save the best move and best score + bestMove, bestScore = move, score + + // Report search statistics to the GUI + fmt.Printf( + "info depth %d seldepth %d score cp %d time %d nodes %d\n", + depth, search.selectiveDepth, bestScore, + elapsedTime.Milliseconds(), + search.nodesSearched, + ) + } + return bestMove +} + +// The top-level function for negamax, which returns a move and a score. +func (search *Search) rootNegamax(depth uint8) (Move, int16) { + + // Reset search statisics + search.nodesSearched = 0 + search.selectiveDepth = 0 + + bestMove := NullMove + alpha, beta := -Inf, Inf + + // Generate the pseduo-legal moves for the current position. + moves := genMoves(&search.Pos) + + // Score the moves + search.scoreMoves(&moves, NullMove, 0) + + for index := 0; index < int(moves.Count); index++ { + + // Order the moves to get the best moves first. + orderMoves(index, &moves) + move := moves.Moves[index] + + // Make the move, and if it was illegal, undo it and skip to the next move. + if !search.Pos.MakeMove(move) { + search.Pos.UnmakeMove(move) + continue + } + + score := -search.negamax(depth-1, 0, -beta, -alpha, true) + search.Pos.UnmakeMove(move) + + // If we have a beta-cutoff (i.e this move gives us a score better than what + // our opponet can already guarantee early in the tree), return beta and the move + // that caused the cutoff as the best move. + if score == beta { + alpha = beta + bestMove = move + break + } + + // If the score of this move is better than alpha (i.e better than the score + // we can currently guarantee), set alpha to be the score and the best move + // to be the move that raised alpha. + if score > alpha { + alpha = score + bestMove = move + // do_pvs = true + } + } + + // Return the best move, and it's score. + return bestMove, alpha +} + +// The primary negamax function, which only returns a score and no best move. +func (search *Search) negamax(depth, ply uint8, alpha, beta int16, do_null bool) int16 { + // Every 2048 nodes, check if our time has expired. + if (search.nodesSearched&2047) == 0 && search.Timer.Check() { + return 0 + } + + // Update the number of nodes searched. + search.nodesSearched++ + + // Check extension extends the search depth by one if we're in check, + // so that we're less likely to push danger over the search horizon. + inCheck := sqIsAttacked( + &search.Pos, + search.Pos.SideToMove, + search.Pos.PieceBB[search.Pos.SideToMove][King].Msb()) + + if inCheck { + depth++ + } + + // If we've reached a search depth of zero, enter quiescence + // search. + if depth <= 0 { + return search.quiescence(alpha, beta, ply, 0) + } + + // Don't do any extra work if the current position is a draw. We + // can just return a draw value. + if search.isDraw() { + return search.contempt() + } + + // Create a variable to store the possible best move we'll get from probing the transposition + // table. And the best move we'll get from the search if we don't get a hit. + ttBestMove := NullMove + + // Probe the transposition table to see if we have a useable matching entry for the current + // position. + score := search.TransTable.Probe(search.Pos.Hash, ply, depth, alpha, beta, &ttBestMove) + if score != Invalid && ply != 0 { + // If we get a hit, return the score and stop searching. + return score + } + + // Do null-move pruning: + // + // https://www.chessprogramming.org/Null_Move_Pruning + // + // If our opponet is given a free move, can they improve their position? If we do a quick + // search after giving our opponet this free move and we still find a move with a score better + // than beta, our opponet can't improve their position and they wouldn't take this path, so we + // have a beta cut-off and can prune this branch. + // + if do_null && !inCheck && depth >= 3 { + // Do the null move. + search.Pos.MakeNullMove() + score := -search.negamax(depth-2-1, ply+1, -beta, -beta+1, false) + search.Pos.UnmakeNullMove() + + // If we've run out of time, abort the search. + if search.Timer.Check() { + return 0 + } + + // If we get a beta cut-off, and it's not a checkmate score, + // we can use the beta cut-off to send the search and avoid + // wasting anymore time. + if score >= beta && abs(score) < Checkmate { + return beta + } + } + + // Generate the moves for the current position. + moves := genMoves(&search.Pos) + noMoves := true + + // Score the moves + search.scoreMoves(&moves, ttBestMove, ply) + + // Set the transposition table entry flag for this node to alpha by default, + // assuming that we won't raise alpha, and create a variable to store the best + // move. + ttFlag := AlphaFlag + + for index := 0; index < int(moves.Count); index++ { + + // Order the moves to get the best moves first. + orderMoves(index, &moves) + move := moves.Moves[index] + + // Make the move, and if it was illegal, undo it and skip to the next move. + if !search.Pos.MakeMove(move) { + search.Pos.UnmakeMove(move) + continue + } + + score := -search.negamax(depth-1, ply+1, -beta, -alpha, true) + search.Pos.UnmakeMove(move) + noMoves = false + + // If we have a beta-cutoff (i.e this move gives us a score better than what + // our opponet can already guarantee early in the tree), return beta and the move + // that caused the cutoff as the best move. + if score >= beta { + alpha = beta + + // Store the killer move for this ply + search.storeKiller(ply, move) + + // Set the transposition table flag to beta and record the + // best move. + ttFlag = BetaFlag + ttBestMove = move + break + } + + // If the score of this move is better than alpha (i.e better than the score + // we can currently guarantee), set alpha to be the score and the best move + // to be the move that raised alpha. + if score > alpha { + alpha = score + + // Set the transposition table flag to exact and record the + // best move. + ttFlag = ExactFlag + ttBestMove = move + + // do_pvs = true + } + } + + // If we don't have any legal moves, it's either checkmate, or a stalemate. + if noMoves { + if inCheck { + // If its checkmate, return a checkmate score of negative infinity, + // with the current ply added to it. That way, the engine will be + // rewarded for finding mate quicker, or avoiding mate longer. + return -Inf + int16(ply) + } else { + // If it's a draw, return the draw value. + return search.contempt() + } + } + + // Store the result of the search for this position only if we haven't run out of time. + if !search.Timer.Check() { + search.TransTable.Store(search.Pos.Hash, ply, depth, alpha, ttFlag, ttBestMove) + } + + // Return the best score, which is alpha. + return alpha +} + +// Onece we reach a depth of zero in the main negamax search, instead of +// returning a static evaluation right away, continue to search deeper +// until the position is quiet (i.e there are no winning tatical captures). +// Doing this is known as quiescence search, and it makes the static evaluation +// much more accurate. +func (search *Search) quiescence(alpha, beta int16, negamax_ply uint8, ply uint8) int16 { + // Every 2048 nodes, check if our time has expired. + if (search.nodesSearched&2047) == 0 && search.Timer.Check() { + return 0 + } + + // Update the number of nodes searched. + search.nodesSearched++ + + // Get a static evaluation score for the position. + score := evaluatePos(&search.Pos) + + // If the score is greater than beta, what our opponet can + // already guarantee early in the search tree, then we + // have a beta-cutoff. + if score >= beta { + // Update the seldepth to report to the UCI before + // we return. + if ply > search.selectiveDepth { + search.selectiveDepth = ply + } + return beta + } + + // If the score is greater than alpha, what score we can guarantee + // to get, raise alpha. + if score > alpha { + alpha = score + } + + // Generate all the captures for the current position. + captures := genMoves(&search.Pos) + + // Score the moves + search.scoreMoves(&captures, NullMove, negamax_ply) + + for index := 0; index < int(captures.Count); index++ { + if captures.Moves[index].MoveType() == Attack { + + // Order the moves to get the best moves first. + orderMoves(index, &captures) + move := captures.Moves[index] + + // Make the move, and if it was illegal, undo it and skip to the next move. + if !search.Pos.MakeMove(move) { + search.Pos.UnmakeMove(move) + continue + } + + // While the position is not quiet, continue + // to search new captures and tatical sequences. + score := -search.quiescence(-beta, -alpha, negamax_ply, ply+1) + search.Pos.UnmakeMove(move) + + // If we have a beta-cutoff (i.e this move gives us a score better than what + // our opponet can already guarantee early in the tree), return beta and the move + // that caused the cutoff as the best move. + if score >= beta { + alpha = beta + break + } + + // If the score of this move is better than alpha (i.e better than the score + // we can currently guarantee), set alpha to be the score and the best move + // to be the move that raised alpha. + if score > alpha { + alpha = score + } + } + } + + // Update the seldepth to report to the UCI + // before we return. + if ply > search.selectiveDepth { + search.selectiveDepth = ply + } + + // Return the best score, which is alpha. + return alpha +} + +// Given a "killer move" (a quiet move that caused a beta cut-off), store the +// Move in the slot for the given depth. +func (search *Search) storeKiller(ply uint8, move Move) { + if move.MoveType() != Attack { + if !move.Equal(search.killerMoves[ply][0]) { + search.killerMoves[ply][1] = search.killerMoves[ply][0] + search.killerMoves[ply][0] = move + } + } +} + +// Determine if the current position is a draw. +func (search *Search) isDraw() bool { + if search.Pos.Rule50 >= 100 { + return true + } + return search.isRepition() +} + +// Determine if the current board state is being repeated. +func (search *Search) isRepition() bool { + var repPly uint16 + for repPly = 0; repPly < search.Pos.HistoryPly; repPly++ { + if search.Pos.History[repPly] == search.Pos.Hash { + return true + } + } + return false +} + +// Determine the draw score based on whose moving. If the engine is moving, +// return a negative score, and if the opponet is moving, return a positive +// score. +func (search *Search) contempt() int16 { + if search.Pos.SideToMove == search.engineColor { + return Draw + } + return -Draw +} + +// Score the moves given +func (search *Search) scoreMoves(moves *MoveList, ttBestMove Move, ply uint8) { + for index := 0; index < int(moves.Count); index++ { + move := &moves.Moves[index] + + if ttBestMove.Equal(*move) { + move.AddScore(TT_BestMoveScore) + } else if search.killerMoves[ply][0].Equal(*move) { + move.AddScore(FirstKillerMoveScore) + } else if search.killerMoves[ply][1].Equal(*move) { + move.AddScore(SecondKillerMoveScore) + } else { + captured := &search.Pos.Squares[move.ToSq()] + moved := &search.Pos.Squares[move.FromSq()] + move.AddScore(MvvLva[captured.Type][moved.Type]) + } + } +} + +// Order the moves given by finding the best move and putting it +// at the index given. +func orderMoves(index int, moves *MoveList) { + bestIndex := index + bestScore := moves.Moves[bestIndex].Score() + + for index := bestIndex; index < int(moves.Count); index++ { + if moves.Moves[index].Score() > bestScore { + bestIndex = index + bestScore = (*moves).Moves[index].Score() + } + } + + tempMove := moves.Moves[index] + moves.Moves[index] = moves.Moves[bestIndex] + moves.Moves[bestIndex] = tempMove +} diff --git a/engine/tables.go b/engine/tables.go index 3e7ffa1..0f9bcb4 100644 --- a/engine/tables.go +++ b/engine/tables.go @@ -1,6 +1,8 @@ package engine -import "math/bits" +import ( + "math/bits" +) // A file containing various precomputed tables used // in the engine. @@ -32,7 +34,7 @@ const ( type Magic struct { MagicNo uint64 Mask Bitboard - Shift int + Shift uint8 } var RookMagics [64]Magic @@ -41,7 +43,8 @@ var BishopMagics [64]Magic var RookAttacks [64][4096]Bitboard var BishopAttacks [64][512]Bitboard -var LinesBewteen [64][64]Bitboard +var Lines [64][64]Bitboard +var Between [64][64]Bitboard var RookMagicNumbers [64]uint64 = [64]uint64{ 0x2220a09401006042, 0x1022000403018802, 0x2422006150081c02, 0x400200102014081a, @@ -229,305 +232,8 @@ var MaskFile [8]Bitboard = [8]Bitboard{ 0x101010101010101, } -var Rays [8][64]Bitboard = [8][64]Bitboard{ - // North rays - { - 0x80808080808080, 0x40404040404040, 0x20202020202020, 0x10101010101010, 0x8080808080808, 0x4040404040404, 0x2020202020202, 0x1010101010101, - 0x808080808080, 0x404040404040, 0x202020202020, 0x101010101010, 0x80808080808, 0x40404040404, 0x20202020202, 0x10101010101, - 0x8080808080, 0x4040404040, 0x2020202020, 0x1010101010, 0x808080808, 0x404040404, 0x202020202, 0x101010101, - 0x80808080, 0x40404040, 0x20202020, 0x10101010, 0x8080808, 0x4040404, 0x2020202, 0x1010101, - 0x808080, 0x404040, 0x202020, 0x101010, 0x80808, 0x40404, 0x20202, 0x10101, - 0x8080, 0x4040, 0x2020, 0x1010, 0x808, 0x404, 0x202, 0x101, - 0x80, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0x1, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - }, - - // South rays - { - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x8000000000000000, 0x4000000000000000, 0x2000000000000000, 0x1000000000000000, 0x800000000000000, 0x400000000000000, 0x200000000000000, 0x100000000000000, - 0x8080000000000000, 0x4040000000000000, 0x2020000000000000, 0x1010000000000000, 0x808000000000000, 0x404000000000000, 0x202000000000000, 0x101000000000000, - 0x8080800000000000, 0x4040400000000000, 0x2020200000000000, 0x1010100000000000, 0x808080000000000, 0x404040000000000, 0x202020000000000, 0x101010000000000, - 0x8080808000000000, 0x4040404000000000, 0x2020202000000000, 0x1010101000000000, 0x808080800000000, 0x404040400000000, 0x202020200000000, 0x101010100000000, - 0x8080808080000000, 0x4040404040000000, 0x2020202020000000, 0x1010101010000000, 0x808080808000000, 0x404040404000000, 0x202020202000000, 0x101010101000000, - 0x8080808080800000, 0x4040404040400000, 0x2020202020200000, 0x1010101010100000, 0x808080808080000, 0x404040404040000, 0x202020202020000, 0x101010101010000, - 0x8080808080808000, 0x4040404040404000, 0x2020202020202000, 0x1010101010101000, 0x808080808080800, 0x404040404040400, 0x202020202020200, 0x101010101010100, - }, - - // East rays - { - 0x7f00000000000000, 0x3f00000000000000, 0x1f00000000000000, 0xf00000000000000, 0x700000000000000, 0x300000000000000, 0x100000000000000, 0x0, - 0x7f000000000000, 0x3f000000000000, 0x1f000000000000, 0xf000000000000, 0x7000000000000, 0x3000000000000, 0x1000000000000, 0x0, - 0x7f0000000000, 0x3f0000000000, 0x1f0000000000, 0xf0000000000, 0x70000000000, 0x30000000000, 0x10000000000, 0x0, - 0x7f00000000, 0x3f00000000, 0x1f00000000, 0xf00000000, 0x700000000, 0x300000000, 0x100000000, 0x0, - 0x7f000000, 0x3f000000, 0x1f000000, 0xf000000, 0x7000000, 0x3000000, 0x1000000, 0x0, - 0x7f0000, 0x3f0000, 0x1f0000, 0xf0000, 0x70000, 0x30000, 0x10000, 0x0, - 0x7f00, 0x3f00, 0x1f00, 0xf00, 0x700, 0x300, 0x100, 0x0, - 0x7f, 0x3f, 0x1f, 0xf, 0x7, 0x3, 0x1, 0x0, - }, - - // West rays - { - 0x0, 0x8000000000000000, 0xc000000000000000, 0xe000000000000000, 0xf000000000000000, 0xf800000000000000, 0xfc00000000000000, 0xfe00000000000000, - 0x0, 0x80000000000000, 0xc0000000000000, 0xe0000000000000, 0xf0000000000000, 0xf8000000000000, 0xfc000000000000, 0xfe000000000000, - 0x0, 0x800000000000, 0xc00000000000, 0xe00000000000, 0xf00000000000, 0xf80000000000, 0xfc0000000000, 0xfe0000000000, - 0x0, 0x8000000000, 0xc000000000, 0xe000000000, 0xf000000000, 0xf800000000, 0xfc00000000, 0xfe00000000, - 0x0, 0x80000000, 0xc0000000, 0xe0000000, 0xf0000000, 0xf8000000, 0xfc000000, 0xfe000000, - 0x0, 0x800000, 0xc00000, 0xe00000, 0xf00000, 0xf80000, 0xfc0000, 0xfe0000, - 0x0, 0x8000, 0xc000, 0xe000, 0xf000, 0xf800, 0xfc00, 0xfe00, - 0x0, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe, - }, - - // Northeast rays - { - 0x40201008040201, 0x20100804020100, 0x10080402010000, 0x8040201000000, 0x4020100000000, 0x2010000000000, 0x1000000000000, 0x0, - 0x402010080402, 0x201008040201, 0x100804020100, 0x80402010000, 0x40201000000, 0x20100000000, 0x10000000000, 0x0, - 0x4020100804, 0x2010080402, 0x1008040201, 0x804020100, 0x402010000, 0x201000000, 0x100000000, 0x0, - 0x40201008, 0x20100804, 0x10080402, 0x8040201, 0x4020100, 0x2010000, 0x1000000, 0x0, - 0x402010, 0x201008, 0x100804, 0x80402, 0x40201, 0x20100, 0x10000, 0x0, - 0x4020, 0x2010, 0x1008, 0x804, 0x402, 0x201, 0x100, 0x0, - 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0x1, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - }, - - // Northwest rays - { - 0x0, 0x80000000000000, 0x40800000000000, 0x20408000000000, 0x10204080000000, 0x8102040800000, 0x4081020408000, 0x2040810204080, - 0x0, 0x800000000000, 0x408000000000, 0x204080000000, 0x102040800000, 0x81020408000, 0x40810204080, 0x20408102040, - 0x0, 0x8000000000, 0x4080000000, 0x2040800000, 0x1020408000, 0x810204080, 0x408102040, 0x204081020, - 0x0, 0x80000000, 0x40800000, 0x20408000, 0x10204080, 0x8102040, 0x4081020, 0x2040810, - 0x0, 0x800000, 0x408000, 0x204080, 0x102040, 0x81020, 0x40810, 0x20408, - 0x0, 0x8000, 0x4080, 0x2040, 0x1020, 0x810, 0x408, 0x204, - 0x0, 0x80, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - }, - - // Southeast rays - { - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x4000000000000000, 0x2000000000000000, 0x1000000000000000, 0x800000000000000, 0x400000000000000, 0x200000000000000, 0x100000000000000, 0x0, - 0x2040000000000000, 0x1020000000000000, 0x810000000000000, 0x408000000000000, 0x204000000000000, 0x102000000000000, 0x1000000000000, 0x0, - 0x1020400000000000, 0x810200000000000, 0x408100000000000, 0x204080000000000, 0x102040000000000, 0x1020000000000, 0x10000000000, 0x0, - 0x810204000000000, 0x408102000000000, 0x204081000000000, 0x102040800000000, 0x1020400000000, 0x10200000000, 0x100000000, 0x0, - 0x408102040000000, 0x204081020000000, 0x102040810000000, 0x1020408000000, 0x10204000000, 0x102000000, 0x1000000, 0x0, - 0x204081020400000, 0x102040810200000, 0x1020408100000, 0x10204080000, 0x102040000, 0x1020000, 0x10000, 0x0, - 0x102040810204000, 0x1020408102000, 0x10204081000, 0x102040800, 0x1020400, 0x10200, 0x100, 0x0, - }, - - // Southwest rays - { - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x8000000000000000, 0x4000000000000000, 0x2000000000000000, 0x1000000000000000, 0x800000000000000, 0x400000000000000, 0x200000000000000, - 0x0, 0x80000000000000, 0x8040000000000000, 0x4020000000000000, 0x2010000000000000, 0x1008000000000000, 0x804000000000000, 0x402000000000000, - 0x0, 0x800000000000, 0x80400000000000, 0x8040200000000000, 0x4020100000000000, 0x2010080000000000, 0x1008040000000000, 0x804020000000000, - 0x0, 0x8000000000, 0x804000000000, 0x80402000000000, 0x8040201000000000, 0x4020100800000000, 0x2010080400000000, 0x1008040200000000, - 0x0, 0x80000000, 0x8040000000, 0x804020000000, 0x80402010000000, 0x8040201008000000, 0x4020100804000000, 0x2010080402000000, - 0x0, 0x800000, 0x80400000, 0x8040200000, 0x804020100000, 0x80402010080000, 0x8040201008040000, 0x4020100804020000, - 0x0, 0x8000, 0x804000, 0x80402000, 0x8040201000, 0x804020100800, 0x80402010080400, 0x8040201008040200, - }, -} - -var Random64 [781]uint64 = [781]uint64{ - 0x9D39247E33776D41, 0x2AF7398005AAA5C7, 0x44DB015024623547, 0x9C15F73E62A76AE2, - 0x75834465489C0C89, 0x3290AC3A203001BF, 0x0FBBAD1F61042279, 0xE83A908FF2FB60CA, - 0x0D7E765D58755C10, 0x1A083822CEAFE02D, 0x9605D5F0E25EC3B0, 0xD021FF5CD13A2ED5, - 0x40BDF15D4A672E32, 0x011355146FD56395, 0x5DB4832046F3D9E5, 0x239F8B2D7FF719CC, - 0x05D1A1AE85B49AA1, 0x679F848F6E8FC971, 0x7449BBFF801FED0B, 0x7D11CDB1C3B7ADF0, - 0x82C7709E781EB7CC, 0xF3218F1C9510786C, 0x331478F3AF51BBE6, 0x4BB38DE5E7219443, - 0xAA649C6EBCFD50FC, 0x8DBD98A352AFD40B, 0x87D2074B81D79217, 0x19F3C751D3E92AE1, - 0xB4AB30F062B19ABF, 0x7B0500AC42047AC4, 0xC9452CA81A09D85D, 0x24AA6C514DA27500, - 0x4C9F34427501B447, 0x14A68FD73C910841, 0xA71B9B83461CBD93, 0x03488B95B0F1850F, - 0x637B2B34FF93C040, 0x09D1BC9A3DD90A94, 0x3575668334A1DD3B, 0x735E2B97A4C45A23, - 0x18727070F1BD400B, 0x1FCBACD259BF02E7, 0xD310A7C2CE9B6555, 0xBF983FE0FE5D8244, - 0x9F74D14F7454A824, 0x51EBDC4AB9BA3035, 0x5C82C505DB9AB0FA, 0xFCF7FE8A3430B241, - 0x3253A729B9BA3DDE, 0x8C74C368081B3075, 0xB9BC6C87167C33E7, 0x7EF48F2B83024E20, - 0x11D505D4C351BD7F, 0x6568FCA92C76A243, 0x4DE0B0F40F32A7B8, 0x96D693460CC37E5D, - 0x42E240CB63689F2F, 0x6D2BDCDAE2919661, 0x42880B0236E4D951, 0x5F0F4A5898171BB6, - 0x39F890F579F92F88, 0x93C5B5F47356388B, 0x63DC359D8D231B78, 0xEC16CA8AEA98AD76, - 0x5355F900C2A82DC7, 0x07FB9F855A997142, 0x5093417AA8A7ED5E, 0x7BCBC38DA25A7F3C, - 0x19FC8A768CF4B6D4, 0x637A7780DECFC0D9, 0x8249A47AEE0E41F7, 0x79AD695501E7D1E8, - 0x14ACBAF4777D5776, 0xF145B6BECCDEA195, 0xDABF2AC8201752FC, 0x24C3C94DF9C8D3F6, - 0xBB6E2924F03912EA, 0x0CE26C0B95C980D9, 0xA49CD132BFBF7CC4, 0xE99D662AF4243939, - 0x27E6AD7891165C3F, 0x8535F040B9744FF1, 0x54B3F4FA5F40D873, 0x72B12C32127FED2B, - 0xEE954D3C7B411F47, 0x9A85AC909A24EAA1, 0x70AC4CD9F04F21F5, 0xF9B89D3E99A075C2, - 0x87B3E2B2B5C907B1, 0xA366E5B8C54F48B8, 0xAE4A9346CC3F7CF2, 0x1920C04D47267BBD, - 0x87BF02C6B49E2AE9, 0x092237AC237F3859, 0xFF07F64EF8ED14D0, 0x8DE8DCA9F03CC54E, - 0x9C1633264DB49C89, 0xB3F22C3D0B0B38ED, 0x390E5FB44D01144B, 0x5BFEA5B4712768E9, - 0x1E1032911FA78984, 0x9A74ACB964E78CB3, 0x4F80F7A035DAFB04, 0x6304D09A0B3738C4, - 0x2171E64683023A08, 0x5B9B63EB9CEFF80C, 0x506AACF489889342, 0x1881AFC9A3A701D6, - 0x6503080440750644, 0xDFD395339CDBF4A7, 0xEF927DBCF00C20F2, 0x7B32F7D1E03680EC, - 0xB9FD7620E7316243, 0x05A7E8A57DB91B77, 0xB5889C6E15630A75, 0x4A750A09CE9573F7, - 0xCF464CEC899A2F8A, 0xF538639CE705B824, 0x3C79A0FF5580EF7F, 0xEDE6C87F8477609D, - 0x799E81F05BC93F31, 0x86536B8CF3428A8C, 0x97D7374C60087B73, 0xA246637CFF328532, - 0x043FCAE60CC0EBA0, 0x920E449535DD359E, 0x70EB093B15B290CC, 0x73A1921916591CBD, - 0x56436C9FE1A1AA8D, 0xEFAC4B70633B8F81, 0xBB215798D45DF7AF, 0x45F20042F24F1768, - 0x930F80F4E8EB7462, 0xFF6712FFCFD75EA1, 0xAE623FD67468AA70, 0xDD2C5BC84BC8D8FC, - 0x7EED120D54CF2DD9, 0x22FE545401165F1C, 0xC91800E98FB99929, 0x808BD68E6AC10365, - 0xDEC468145B7605F6, 0x1BEDE3A3AEF53302, 0x43539603D6C55602, 0xAA969B5C691CCB7A, - 0xA87832D392EFEE56, 0x65942C7B3C7E11AE, 0xDED2D633CAD004F6, 0x21F08570F420E565, - 0xB415938D7DA94E3C, 0x91B859E59ECB6350, 0x10CFF333E0ED804A, 0x28AED140BE0BB7DD, - 0xC5CC1D89724FA456, 0x5648F680F11A2741, 0x2D255069F0B7DAB3, 0x9BC5A38EF729ABD4, - 0xEF2F054308F6A2BC, 0xAF2042F5CC5C2858, 0x480412BAB7F5BE2A, 0xAEF3AF4A563DFE43, - 0x19AFE59AE451497F, 0x52593803DFF1E840, 0xF4F076E65F2CE6F0, 0x11379625747D5AF3, - 0xBCE5D2248682C115, 0x9DA4243DE836994F, 0x066F70B33FE09017, 0x4DC4DE189B671A1C, - 0x51039AB7712457C3, 0xC07A3F80C31FB4B4, 0xB46EE9C5E64A6E7C, 0xB3819A42ABE61C87, - 0x21A007933A522A20, 0x2DF16F761598AA4F, 0x763C4A1371B368FD, 0xF793C46702E086A0, - 0xD7288E012AEB8D31, 0xDE336A2A4BC1C44B, 0x0BF692B38D079F23, 0x2C604A7A177326B3, - 0x4850E73E03EB6064, 0xCFC447F1E53C8E1B, 0xB05CA3F564268D99, 0x9AE182C8BC9474E8, - 0xA4FC4BD4FC5558CA, 0xE755178D58FC4E76, 0x69B97DB1A4C03DFE, 0xF9B5B7C4ACC67C96, - 0xFC6A82D64B8655FB, 0x9C684CB6C4D24417, 0x8EC97D2917456ED0, 0x6703DF9D2924E97E, - 0xC547F57E42A7444E, 0x78E37644E7CAD29E, 0xFE9A44E9362F05FA, 0x08BD35CC38336615, - 0x9315E5EB3A129ACE, 0x94061B871E04DF75, 0xDF1D9F9D784BA010, 0x3BBA57B68871B59D, - 0xD2B7ADEEDED1F73F, 0xF7A255D83BC373F8, 0xD7F4F2448C0CEB81, 0xD95BE88CD210FFA7, - 0x336F52F8FF4728E7, 0xA74049DAC312AC71, 0xA2F61BB6E437FDB5, 0x4F2A5CB07F6A35B3, - 0x87D380BDA5BF7859, 0x16B9F7E06C453A21, 0x7BA2484C8A0FD54E, 0xF3A678CAD9A2E38C, - 0x39B0BF7DDE437BA2, 0xFCAF55C1BF8A4424, 0x18FCF680573FA594, 0x4C0563B89F495AC3, - 0x40E087931A00930D, 0x8CFFA9412EB642C1, 0x68CA39053261169F, 0x7A1EE967D27579E2, - 0x9D1D60E5076F5B6F, 0x3810E399B6F65BA2, 0x32095B6D4AB5F9B1, 0x35CAB62109DD038A, - 0xA90B24499FCFAFB1, 0x77A225A07CC2C6BD, 0x513E5E634C70E331, 0x4361C0CA3F692F12, - 0xD941ACA44B20A45B, 0x528F7C8602C5807B, 0x52AB92BEB9613989, 0x9D1DFA2EFC557F73, - 0x722FF175F572C348, 0x1D1260A51107FE97, 0x7A249A57EC0C9BA2, 0x04208FE9E8F7F2D6, - 0x5A110C6058B920A0, 0x0CD9A497658A5698, 0x56FD23C8F9715A4C, 0x284C847B9D887AAE, - 0x04FEABFBBDB619CB, 0x742E1E651C60BA83, 0x9A9632E65904AD3C, 0x881B82A13B51B9E2, - 0x506E6744CD974924, 0xB0183DB56FFC6A79, 0x0ED9B915C66ED37E, 0x5E11E86D5873D484, - 0xF678647E3519AC6E, 0x1B85D488D0F20CC5, 0xDAB9FE6525D89021, 0x0D151D86ADB73615, - 0xA865A54EDCC0F019, 0x93C42566AEF98FFB, 0x99E7AFEABE000731, 0x48CBFF086DDF285A, - 0x7F9B6AF1EBF78BAF, 0x58627E1A149BBA21, 0x2CD16E2ABD791E33, 0xD363EFF5F0977996, - 0x0CE2A38C344A6EED, 0x1A804AADB9CFA741, 0x907F30421D78C5DE, 0x501F65EDB3034D07, - 0x37624AE5A48FA6E9, 0x957BAF61700CFF4E, 0x3A6C27934E31188A, 0xD49503536ABCA345, - 0x088E049589C432E0, 0xF943AEE7FEBF21B8, 0x6C3B8E3E336139D3, 0x364F6FFA464EE52E, - 0xD60F6DCEDC314222, 0x56963B0DCA418FC0, 0x16F50EDF91E513AF, 0xEF1955914B609F93, - 0x565601C0364E3228, 0xECB53939887E8175, 0xBAC7A9A18531294B, 0xB344C470397BBA52, - 0x65D34954DAF3CEBD, 0xB4B81B3FA97511E2, 0xB422061193D6F6A7, 0x071582401C38434D, - 0x7A13F18BBEDC4FF5, 0xBC4097B116C524D2, 0x59B97885E2F2EA28, 0x99170A5DC3115544, - 0x6F423357E7C6A9F9, 0x325928EE6E6F8794, 0xD0E4366228B03343, 0x565C31F7DE89EA27, - 0x30F5611484119414, 0xD873DB391292ED4F, 0x7BD94E1D8E17DEBC, 0xC7D9F16864A76E94, - 0x947AE053EE56E63C, 0xC8C93882F9475F5F, 0x3A9BF55BA91F81CA, 0xD9A11FBB3D9808E4, - 0x0FD22063EDC29FCA, 0xB3F256D8ACA0B0B9, 0xB03031A8B4516E84, 0x35DD37D5871448AF, - 0xE9F6082B05542E4E, 0xEBFAFA33D7254B59, 0x9255ABB50D532280, 0xB9AB4CE57F2D34F3, - 0x693501D628297551, 0xC62C58F97DD949BF, 0xCD454F8F19C5126A, 0xBBE83F4ECC2BDECB, - 0xDC842B7E2819E230, 0xBA89142E007503B8, 0xA3BC941D0A5061CB, 0xE9F6760E32CD8021, - 0x09C7E552BC76492F, 0x852F54934DA55CC9, 0x8107FCCF064FCF56, 0x098954D51FFF6580, - 0x23B70EDB1955C4BF, 0xC330DE426430F69D, 0x4715ED43E8A45C0A, 0xA8D7E4DAB780A08D, - 0x0572B974F03CE0BB, 0xB57D2E985E1419C7, 0xE8D9ECBE2CF3D73F, 0x2FE4B17170E59750, - 0x11317BA87905E790, 0x7FBF21EC8A1F45EC, 0x1725CABFCB045B00, 0x964E915CD5E2B207, - 0x3E2B8BCBF016D66D, 0xBE7444E39328A0AC, 0xF85B2B4FBCDE44B7, 0x49353FEA39BA63B1, - 0x1DD01AAFCD53486A, 0x1FCA8A92FD719F85, 0xFC7C95D827357AFA, 0x18A6A990C8B35EBD, - 0xCCCB7005C6B9C28D, 0x3BDBB92C43B17F26, 0xAA70B5B4F89695A2, 0xE94C39A54A98307F, - 0xB7A0B174CFF6F36E, 0xD4DBA84729AF48AD, 0x2E18BC1AD9704A68, 0x2DE0966DAF2F8B1C, - 0xB9C11D5B1E43A07E, 0x64972D68DEE33360, 0x94628D38D0C20584, 0xDBC0D2B6AB90A559, - 0xD2733C4335C6A72F, 0x7E75D99D94A70F4D, 0x6CED1983376FA72B, 0x97FCAACBF030BC24, - 0x7B77497B32503B12, 0x8547EDDFB81CCB94, 0x79999CDFF70902CB, 0xCFFE1939438E9B24, - 0x829626E3892D95D7, 0x92FAE24291F2B3F1, 0x63E22C147B9C3403, 0xC678B6D860284A1C, - 0x5873888850659AE7, 0x0981DCD296A8736D, 0x9F65789A6509A440, 0x9FF38FED72E9052F, - 0xE479EE5B9930578C, 0xE7F28ECD2D49EECD, 0x56C074A581EA17FE, 0x5544F7D774B14AEF, - 0x7B3F0195FC6F290F, 0x12153635B2C0CF57, 0x7F5126DBBA5E0CA7, 0x7A76956C3EAFB413, - 0x3D5774A11D31AB39, 0x8A1B083821F40CB4, 0x7B4A38E32537DF62, 0x950113646D1D6E03, - 0x4DA8979A0041E8A9, 0x3BC36E078F7515D7, 0x5D0A12F27AD310D1, 0x7F9D1A2E1EBE1327, - 0xDA3A361B1C5157B1, 0xDCDD7D20903D0C25, 0x36833336D068F707, 0xCE68341F79893389, - 0xAB9090168DD05F34, 0x43954B3252DC25E5, 0xB438C2B67F98E5E9, 0x10DCD78E3851A492, - 0xDBC27AB5447822BF, 0x9B3CDB65F82CA382, 0xB67B7896167B4C84, 0xBFCED1B0048EAC50, - 0xA9119B60369FFEBD, 0x1FFF7AC80904BF45, 0xAC12FB171817EEE7, 0xAF08DA9177DDA93D, - 0x1B0CAB936E65C744, 0xB559EB1D04E5E932, 0xC37B45B3F8D6F2BA, 0xC3A9DC228CAAC9E9, - 0xF3B8B6675A6507FF, 0x9FC477DE4ED681DA, 0x67378D8ECCEF96CB, 0x6DD856D94D259236, - 0xA319CE15B0B4DB31, 0x073973751F12DD5E, 0x8A8E849EB32781A5, 0xE1925C71285279F5, - 0x74C04BF1790C0EFE, 0x4DDA48153C94938A, 0x9D266D6A1CC0542C, 0x7440FB816508C4FE, - 0x13328503DF48229F, 0xD6BF7BAEE43CAC40, 0x4838D65F6EF6748F, 0x1E152328F3318DEA, - 0x8F8419A348F296BF, 0x72C8834A5957B511, 0xD7A023A73260B45C, 0x94EBC8ABCFB56DAE, - 0x9FC10D0F989993E0, 0xDE68A2355B93CAE6, 0xA44CFE79AE538BBE, 0x9D1D84FCCE371425, - 0x51D2B1AB2DDFB636, 0x2FD7E4B9E72CD38C, 0x65CA5B96B7552210, 0xDD69A0D8AB3B546D, - 0x604D51B25FBF70E2, 0x73AA8A564FB7AC9E, 0x1A8C1E992B941148, 0xAAC40A2703D9BEA0, - 0x764DBEAE7FA4F3A6, 0x1E99B96E70A9BE8B, 0x2C5E9DEB57EF4743, 0x3A938FEE32D29981, - 0x26E6DB8FFDF5ADFE, 0x469356C504EC9F9D, 0xC8763C5B08D1908C, 0x3F6C6AF859D80055, - 0x7F7CC39420A3A545, 0x9BFB227EBDF4C5CE, 0x89039D79D6FC5C5C, 0x8FE88B57305E2AB6, - 0xA09E8C8C35AB96DE, 0xFA7E393983325753, 0xD6B6D0ECC617C699, 0xDFEA21EA9E7557E3, - 0xB67C1FA481680AF8, 0xCA1E3785A9E724E5, 0x1CFC8BED0D681639, 0xD18D8549D140CAEA, - 0x4ED0FE7E9DC91335, 0xE4DBF0634473F5D2, 0x1761F93A44D5AEFE, 0x53898E4C3910DA55, - 0x734DE8181F6EC39A, 0x2680B122BAA28D97, 0x298AF231C85BAFAB, 0x7983EED3740847D5, - 0x66C1A2A1A60CD889, 0x9E17E49642A3E4C1, 0xEDB454E7BADC0805, 0x50B704CAB602C329, - 0x4CC317FB9CDDD023, 0x66B4835D9EAFEA22, 0x219B97E26FFC81BD, 0x261E4E4C0A333A9D, - 0x1FE2CCA76517DB90, 0xD7504DFA8816EDBB, 0xB9571FA04DC089C8, 0x1DDC0325259B27DE, - 0xCF3F4688801EB9AA, 0xF4F5D05C10CAB243, 0x38B6525C21A42B0E, 0x36F60E2BA4FA6800, - 0xEB3593803173E0CE, 0x9C4CD6257C5A3603, 0xAF0C317D32ADAA8A, 0x258E5A80C7204C4B, - 0x8B889D624D44885D, 0xF4D14597E660F855, 0xD4347F66EC8941C3, 0xE699ED85B0DFB40D, - 0x2472F6207C2D0484, 0xC2A1E7B5B459AEB5, 0xAB4F6451CC1D45EC, 0x63767572AE3D6174, - 0xA59E0BD101731A28, 0x116D0016CB948F09, 0x2CF9C8CA052F6E9F, 0x0B090A7560A968E3, - 0xABEEDDB2DDE06FF1, 0x58EFC10B06A2068D, 0xC6E57A78FBD986E0, 0x2EAB8CA63CE802D7, - 0x14A195640116F336, 0x7C0828DD624EC390, 0xD74BBE77E6116AC7, 0x804456AF10F5FB53, - 0xEBE9EA2ADF4321C7, 0x03219A39EE587A30, 0x49787FEF17AF9924, 0xA1E9300CD8520548, - 0x5B45E522E4B1B4EF, 0xB49C3B3995091A36, 0xD4490AD526F14431, 0x12A8F216AF9418C2, - 0x001F837CC7350524, 0x1877B51E57A764D5, 0xA2853B80F17F58EE, 0x993E1DE72D36D310, - 0xB3598080CE64A656, 0x252F59CF0D9F04BB, 0xD23C8E176D113600, 0x1BDA0492E7E4586E, - 0x21E0BD5026C619BF, 0x3B097ADAF088F94E, 0x8D14DEDB30BE846E, 0xF95CFFA23AF5F6F4, - 0x3871700761B3F743, 0xCA672B91E9E4FA16, 0x64C8E531BFF53B55, 0x241260ED4AD1E87D, - 0x106C09B972D2E822, 0x7FBA195410E5CA30, 0x7884D9BC6CB569D8, 0x0647DFEDCD894A29, - 0x63573FF03E224774, 0x4FC8E9560F91B123, 0x1DB956E450275779, 0xB8D91274B9E9D4FB, - 0xA2EBEE47E2FBFCE1, 0xD9F1F30CCD97FB09, 0xEFED53D75FD64E6B, 0x2E6D02C36017F67F, - 0xA9AA4D20DB084E9B, 0xB64BE8D8B25396C1, 0x70CB6AF7C2D5BCF0, 0x98F076A4F7A2322E, - 0xBF84470805E69B5F, 0x94C3251F06F90CF3, 0x3E003E616A6591E9, 0xB925A6CD0421AFF3, - 0x61BDD1307C66E300, 0xBF8D5108E27E0D48, 0x240AB57A8B888B20, 0xFC87614BAF287E07, - 0xEF02CDD06FFDB432, 0xA1082C0466DF6C0A, 0x8215E577001332C8, 0xD39BB9C3A48DB6CF, - 0x2738259634305C14, 0x61CF4F94C97DF93D, 0x1B6BACA2AE4E125B, 0x758F450C88572E0B, - 0x959F587D507A8359, 0xB063E962E045F54D, 0x60E8ED72C0DFF5D1, 0x7B64978555326F9F, - 0xFD080D236DA814BA, 0x8C90FD9B083F4558, 0x106F72FE81E2C590, 0x7976033A39F7D952, - 0xA4EC0132764CA04B, 0x733EA705FAE4FA77, 0xB4D8F77BC3E56167, 0x9E21F4F903B33FD9, - 0x9D765E419FB69F6D, 0xD30C088BA61EA5EF, 0x5D94337FBFAF7F5B, 0x1A4E4822EB4D7A59, - 0x6FFE73E81B637FB3, 0xDDF957BC36D8B9CA, 0x64D0E29EEA8838B3, 0x08DD9BDFD96B9F63, - 0x087E79E5A57D1D13, 0xE328E230E3E2B3FB, 0x1C2559E30F0946BE, 0x720BF5F26F4D2EAA, - 0xB0774D261CC609DB, 0x443F64EC5A371195, 0x4112CF68649A260E, 0xD813F2FAB7F5C5CA, - 0x660D3257380841EE, 0x59AC2C7873F910A3, 0xE846963877671A17, 0x93B633ABFA3469F8, - 0xC0C0F5A60EF4CDCF, 0xCAF21ECD4377B28C, 0x57277707199B8175, 0x506C11B9D90E8B1D, - 0xD83CC2687A19255F, 0x4A29C6465A314CD1, 0xED2DF21216235097, 0xB5635C95FF7296E2, - 0x22AF003AB672E811, 0x52E762596BF68235, 0x9AEBA33AC6ECC6B0, 0x944F6DE09134DFB6, - 0x6C47BEC883A7DE39, 0x6AD047C430A12104, 0xA5B1CFDBA0AB4067, 0x7C45D833AFF07862, - 0x5092EF950A16DA0B, 0x9338E69C052B8E7B, 0x455A4B4CFE30E3F5, 0x6B02E63195AD0CF8, - 0x6B17B224BAD6BF27, 0xD1E0CCD25BB9C169, 0xDE0C89A556B9AE70, 0x50065E535A213CF6, - 0x9C1169FA2777B874, 0x78EDEFD694AF1EED, 0x6DC93D9526A50E68, 0xEE97F453F06791ED, - 0x32AB0EDB696703D3, 0x3A6853C7E70757A7, 0x31865CED6120F37D, 0x67FEF95D92607890, - 0x1F2B1D1F15F6DC9C, 0xB69E38A8965C6B65, 0xAA9119FF184CCCF4, 0xF43C732873F24C13, - 0xFB4A3D794A9A80D2, 0x3550C2321FD6109C, 0x371F77E76BB8417E, 0x6BFA9AAE5EC05779, - 0xCD04F3FF001A4778, 0xE3273522064480CA, 0x9F91508BFFCFC14A, 0x049A7F41061A9E60, - 0xFCB6BE43A9F2FE9B, 0x08DE8A1C7797DA9B, 0x8F9887E6078735A1, 0xB5B4071DBFC73A66, - 0x230E343DFBA08D33, 0x43ED7F5A0FAE657D, 0x3A88A0FBBCB05C63, 0x21874B8B4D2DBC4F, - 0x1BDEA12E35F6A8C9, 0x53C065C6C8E63528, 0xE34A1D250E7A8D6B, 0xD6B04D3B7651DD7E, - 0x5E90277E7CB39E2D, 0x2C046F22062DC67D, 0xB10BB459132D0A26, 0x3FA9DDFB67E2F199, - 0x0E09B88E1914F7AF, 0x10E8B35AF3EEAB37, 0x9EEDECA8E272B933, 0xD4C718BC4AE8AE5F, - 0x81536D601170FC20, 0x91B534F885818A06, 0xEC8177F83F900978, 0x190E714FADA5156E, - 0xB592BF39B0364963, 0x89C350C893AE7DC1, 0xAC042E70F8B383F2, 0xB49B52E587A1EE60, - 0xFB152FE3FF26DA89, 0x3E666E6F69AE2C15, 0x3B544EBE544C19F9, 0xE805A1E290CF2456, - 0x24B33C9D7ED25117, 0xE74733427B72F0C1, 0x0A804D18B7097475, 0x57E3306D881EDB4F, - 0x4AE7D6A36EB5DBCB, 0x2D8D5432157064C8, 0xD1E649DE1E7F268B, 0x8A328A1CEDFE552C, - 0x07A3AEC79624C7DA, 0x84547DDC3E203C94, 0x990A98FD5071D263, 0x1A4FF12616EEFC89, - 0xF6F7FD1431714200, 0x30C05B1BA332F41C, 0x8D2636B81555A786, 0x46C9FEB55D120902, - 0xCCEC0A73B49C9921, 0x4E9D2827355FC492, 0x19EBB029435DCB0F, 0x4659D2B743848A2C, - 0x963EF2C96B33BE31, 0x74F85198B05A2E7D, 0x5A0F544DD2B1FB18, 0x03727073C2E134B1, - 0xC7F6AA2DE59AEA61, 0x352787BAA0D7C22F, 0x9853EAB63B5E0B35, 0xABBDCDD7ED5C0860, - 0xCF05DAF5AC8D77B0, 0x49CAD48CEBF4A71E, 0x7A4C10EC2158C4A6, 0xD9E92AA246BF719E, - 0x13AE978D09FE5557, 0x730499AF921549FF, 0x4E4B705B92903BA4, 0xFF577222C14F0A3A, - 0x55B6344CF97AAFAE, 0xB862225B055B6960, 0xCAC09AFBDDD2CDB4, 0xDAF8E9829FE96B5F, - 0xB5FDFC5D3132C498, 0x310CB380DB6F7503, 0xE87FBB46217A360E, 0x2102AE466EBB1148, - 0xF8549E1A3AA5E00D, 0x07A69AFDCC42261A, 0xC4C118BFE78FEAAE, 0xF9F4892ED96BD438, - 0x1AF3DBE25D8F45DA, 0xF5B4B0B0D2DEEEB4, 0x962ACEEFA82E1C84, 0x046E3ECAAF453CE9, - 0xF05D129681949A4C, 0x964781CE734B3C84, 0x9C2ED44081CE5FBD, 0x522E23F3925E319E, - 0x177E00F9FC32F791, 0x2BC60A63A6F3B3F2, 0x222BBFAE61725606, 0x486289DDCC3D6780, - 0x7DC7785B8EFDFC80, 0x8AF38731C02BA980, 0x1FAB64EA29A2DDF7, 0xE4D9429322CD065A, - 0x9DA058C67844F20C, 0x24C0E332B70019B0, 0x233003B5A6CFE6AD, 0xD586BD01C5C217F6, - 0x5E5637885F29BC2B, 0x7EBA726D8C94094B, 0x0A56A5F0BFE39272, 0xD79476A84EE20D06, - 0x9E4C1269BAA4BF37, 0x17EFEE45B0DEE640, 0x1D95B0A5FCF90BC6, 0x93CBE0B699C2585D, - 0x65FA4F227A2B6D79, 0xD5F9E858292504D5, 0xC2B5A03F71471A6F, 0x59300222B4561E00, - 0xCE2F8642CA0712DC, 0x7CA9723FBB2E8988, 0x2785338347F2BA08, 0xC61BB3A141E50E8C, - 0x150F361DAB9DEC26, 0x9F6A419D382595F4, 0x64A53DC924FE7AC9, 0x142DE49FFF7A7C3D, - 0x0C335248857FA9E7, 0x0A9C32D5EAE45305, 0xE6C42178C4BBB92E, 0x71F1CE2490D20B07, - 0xF1BCC3D275AFE51A, 0xE728E8C83C334074, 0x96FBF83A12884624, 0x81A1549FD6573DA5, - 0x5FA7867CAF35E149, 0x56986E2EF3ED091B, 0x917F1DD5F8886C61, 0xD20D8C88C8FFE65F, - 0x31D71DCE64B2C310, 0xF165B587DF898190, 0xA57E6339DD2CF3A0, 0x1EF6E6DBB1961EC9, - 0x70CC73D90BC26E24, 0xE21A6B35DF0C3AD7, 0x003A93D8B2806962, 0x1C99DED33CB890A1, - 0xCF3145DE0ADD4289, 0xD0E4427A5514FB72, 0x77C621CC9FB3A483, 0x67A34DAC4356550B, - 0xF8D626AAAF278509, -} - // Generate a blocker mask for rooks. -func genRookMasks(sq uint8) Bitboard { +func GenRookMasks(sq uint8) Bitboard { slider := SquareBB[sq] sliderPos := slider.Msb() @@ -549,7 +255,7 @@ func genRookMasks(sq uint8) Bitboard { } // Generate a blocker mask for bishops. -func genBishopMasks(sq uint8) Bitboard { +func GenBishopMasks(sq uint8) Bitboard { slider := SquareBB[sq] sliderPos := slider.Msb() @@ -571,7 +277,7 @@ func genBishopMasks(sq uint8) Bitboard { return Bitboard((diagonalMoves | antidiagonalMoves) & edges) } -func genRookAttacks(sq uint8, occupied Bitboard) Bitboard { +func GenRookAttacks(sq uint8, occupied Bitboard) Bitboard { slider := SquareBB[sq] sliderPos := slider.Msb() @@ -593,7 +299,7 @@ func genRookAttacks(sq uint8, occupied Bitboard) Bitboard { } // Generate the moves a bishop has given a square and board occupancy. -func genBishopAttacks(sq uint8, occupied Bitboard) Bitboard { +func GenBishopAttacks(sq uint8, occupied Bitboard) Bitboard { slider := SquareBB[sq] sliderPos := slider.Msb() @@ -621,9 +327,9 @@ func initRookMagics() { magic := &RookMagics[sq] magic.MagicNo = RookMagicNumbers[sq] - magic.Mask = genRookMasks(sq) + magic.Mask = GenRookMasks(sq) no_bits := magic.Mask.CountBits() - magic.Shift = 64 - no_bits + magic.Shift = uint8(64 - no_bits) permutations := make([]Bitboard, 1<> magic.Shift - attacks := genRookAttacks(sq, permutations[idx]) + attacks := GenRookAttacks(sq, permutations[idx]) RookAttacks[sq][index] = attacks } @@ -651,9 +357,9 @@ func initBishopMagics() { magic := &BishopMagics[sq] magic.MagicNo = BishopMagicNumbers[sq] - magic.Mask = genBishopMasks(sq) + magic.Mask = GenBishopMasks(sq) no_bits := magic.Mask.CountBits() - magic.Shift = 64 - no_bits + magic.Shift = uint8(64 - no_bits) permutations := make([]Bitboard, 1<> magic.Shift - attacks := genBishopAttacks(sq, permutations[idx]) + attacks := GenBishopAttacks(sq, permutations[idx]) BishopAttacks[sq][index] = attacks } @@ -675,16 +381,55 @@ func initBishopMagics() { } func init() { + // Initalize the rook and bishop magic bitboard tables. initRookMagics() initBishopMagics() - for sq1 := 0; sq1 < 64; sq1++ { - for direction := 0; direction < 8; direction++ { - rayBetween := Rays[direction][sq1] - for sq2 := 0; sq2 < 64; sq2++ { - if rayBetween.BitSet(uint8(sq2)) { - LinesBewteen[sq1][sq2] = (rayBetween & ^Rays[direction][sq2]) + + // Initalize the Lines array, a two dimensional structure + // that takes two squares, and maps it to a bitboard containg + // the full line (from board edge to board edge) that intersects + // those two squares. If no intersection exists along a diagonal, + // antidiagonal, rank, or file, an empty bitboard is used. + + var sq1, sq2 uint8 + for sq1 = 0; sq1 < 64; sq1++ { + for sq2 = 0; sq2 < 64; sq2++ { + + for idx := 0; idx < 15; idx++ { + diagonal := MaskDiagonal[idx] + antidiagonal := MaskAntidiagonal[idx] + + if diagonal.BitSet(sq1) && diagonal.BitSet(sq2) { + Lines[sq1][sq2] = diagonal + } + + if antidiagonal.BitSet(sq1) && antidiagonal.BitSet(sq2) { + Lines[sq1][sq2] = antidiagonal + } + } + + for idx := 0; idx < 8; idx++ { + file := MaskFile[idx] + rank := MaskRank[idx] + + if file.BitSet(sq1) && file.BitSet(sq2) { + Lines[sq1][sq2] = file + } + + if rank.BitSet(sq1) && rank.BitSet(sq2) { + Lines[sq1][sq2] = rank } } } } + + // Initalize a second two dimensional array, which is like Lines, + // but rather than including the full line, it only includes the + // line between the two squares of interest. + for sq1 = 0; sq1 < 64; sq1++ { + for sq2 = 0; sq2 < 64; sq2++ { + Between[sq1][sq2] = Lines[sq1][sq2] & ((FullBB >> sq1) ^ (FullBB >> sq2)) + Between[sq1][sq2] = Between[sq1][sq2] | SquareBB[sq1] | SquareBB[sq2] + } + } } diff --git a/engine/timemanager.go b/engine/timemanager.go new file mode 100644 index 0000000..5b39933 --- /dev/null +++ b/engine/timemanager.go @@ -0,0 +1,63 @@ +package engine + +// timemanager.go implements the time mangement logic which Blunder +// uses during its search phase. + +import ( + "time" +) + +const NoValue int64 = 0 + +// A struct which holds data for a timer for Blunder's time mangement. +type TimeManager struct { + TimeLeft int64 + Increment int64 + MovesToGo int64 + Stop bool + + stopTime time.Time +} + +// Start the timer, setting up the internal state. +func (tm *TimeManager) Start() { + // Calculate the time we can allocate for the search about to start. + var timeForMove int64 + + if tm.MovesToGo != NoValue { + // If we have a certian amount of moves to go before the time we have left + // is reset, use that value to divide the time currently left. + timeForMove = tm.TimeLeft / tm.MovesToGo + } else { + // Otherwise get 2.5% of the current time left and use that. + timeForMove = tm.TimeLeft / 40 + } + + // Give an bonus from the increment + timeForMove += tm.Increment / 2 + + // If the increment bonus puts us outside of the actual time we + // have left, use the time we have left minus 500ms. + if timeForMove >= tm.TimeLeft { + timeForMove = tm.TimeLeft - 500 + } + + // If taking away 500ms puts us below zero, use 100ms + // to just get a move to return. + if timeForMove <= 0 { + timeForMove = 100 + } + + // Calculate the time from now when we need to stop searching, based on the + // time are allowed to spend on the current search. + tm.stopTime = time.Now().Add(time.Duration(timeForMove) * time.Millisecond) + tm.Stop = false +} + +// Check if the time we alloted for picking this move has expired. +func (tm *TimeManager) Check() bool { + if time.Now().After(tm.stopTime) { + tm.Stop = true + } + return tm.Stop +} diff --git a/engine/transposition.go b/engine/transposition.go new file mode 100644 index 0000000..15e3119 --- /dev/null +++ b/engine/transposition.go @@ -0,0 +1,155 @@ +package engine + +// transposition.go contains an implementation of a transposition table (TT) to use +// in search. + +const ( + // Default size of the transposition table, in MB. + DefaultTTSize = 64 + + // Constant for the size of a transposition table entry, in bytes. + TTEntrySize = 14 + + // Constants representing the different flags for a transposition table entry, + // which determine what kind of entry it is. If the entry has a score from + // a fail-low node (alpha wasn't raised), it's an alpha entry. If the entry has + // a score from a fail-high node (a beta cutoff occured), it's a beta entry. And + // if the entry has an exact score (alpha was raised), it's an exact entry. + AlphaFlag uint8 = iota + BetaFlag + ExactFlag + + // A constant representing an invalid score from probing the transposition table. + // this constant's value doesn't matter as long as it's not in the range of possible + // score values. So it must be outside of the range (-Inf, Inf). + Invalid int16 = 20000 + + // A constant representing the minimum or maximum value that a score from the search + // must be below or above to be a checkmate score. The score assumes that the engine + // will not find mate in 100. + Checkmate = 9000 +) + +// A struct for a transposition table entry. +type TT_Entry struct { + Hash uint64 + Depth uint8 + Score int16 + Flag uint8 + Best Move +} + +// A struct for a transposition table. +type TransTable struct { + entries []TT_Entry + size uint64 +} + +// Resize the transposition table given what the size should be in MB. +func (tt *TransTable) Resize(sizeInMB uint64) { + size := (sizeInMB * 1024 * 1024) / TTEntrySize + tt.entries = make([]TT_Entry, size) + tt.size = size +} + +// Get an entry from the table. +func (tt *TransTable) Probe(hash uint64, ply, depth uint8, alpha, beta int16, best *Move) int16 { + // Get the entry from the table, calculating an index by modulo-ing the hash of + // the position by the size of the table. + entry := tt.entries[hash%tt.size] + + score := Invalid + + // Since index collisions can occur, test if the hash of the entry at this index + // actually matches the hash for the current position. + if entry.Hash == hash { + + // Even if we don't get a score we can use from the table, we can still + // use the best move in this entry and put it first in our move ordering + // scheme. + *best = entry.Best + + // To be able to get an accurate value from this entry, make sure the results of + // this entry are from a search that is equal or greater than the current + // depth of our search. + if entry.Depth >= depth { + if entry.Flag == ExactFlag { + // If we have an exact entry, we can use the saved score. + score = entry.Score + } + + if entry.Flag == AlphaFlag && entry.Score <= alpha { + // If we have an alpha entry, and the entry's score is less than our + // current alpha, then we know that our current alpha is the best score + // we can get in this node, so we can stop searching and use alpha. + score = alpha + } + + if entry.Flag == BetaFlag && entry.Score >= beta { + // If we have a beta entry, and the entry's score is greater than our + // current beta, then we have a beta-cutoff, since while + // searching this node previously, we found a value greater than the current + // beta. so we can stop searching and use beta. + score = beta + } + + // If the score we get from the transposition table is a checkmate score, we need + // to do a little extra work. This is because we store checkmates in the table using + // their distance from the node they're found in, not their distance from the root. + // So if we found a checkmate-in-8 in a node that was 5 plies from the root, we need + // to store the score as a checkmate-in-3. Then, if we read the checkmate-in-3 from + // the table in a node that's 4 plies from the root, we need to return the score as + // checkmate-in-7. + if score != Invalid && score > Checkmate { + score -= int16(ply) + } + + if score != Invalid && score < -Checkmate { + score += int16(ply) + } + } + } + + // Return the score + return score +} + +// Store an entry in the table. +func (tt *TransTable) Store(hash uint64, ply, depth uint8, score int16, flag uint8, best Move) { + entry := &tt.entries[hash%tt.size] + entry.Hash = hash + entry.Depth = depth + entry.Flag = flag + entry.Best = best + + // If the score we get from the transposition table is a checkmate score, we need + // to do a little extra work. This is because we store checkmates in the table using + // their distance from the node they're found in, not their distance from the root. + // So if we found a checkmate-in-8 in a node that was 5 plies from the root, we need + // to store the score as a checkmate-in-3. Then, if we read the checkmate-in-3 from + // the table in a node that's 4 plies from the root, we need to return the score as + // checkmate-in-7. + if score > Checkmate { + score += int16(ply) + } + + if score < -Checkmate { + score -= int16(ply) + } + + entry.Score = score +} + +// Unitialize the memory used by the transposition table +func (tt *TransTable) Unitialize() { + tt.entries = nil + tt.size = 0 +} + +// Clear the transposition table +func (tt *TransTable) Clear() { + var idx uint64 + for idx = 0; idx < tt.size; idx++ { + tt.entries[idx] = TT_Entry{} + } +} diff --git a/engine/uci.go b/engine/uci.go new file mode 100644 index 0000000..5db2798 --- /dev/null +++ b/engine/uci.go @@ -0,0 +1,185 @@ +package engine + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +const ( + EngineName = "Blunder 5.0.0" + EngineAuthor = "Christian Dean" + EngineEmail = "deanmchris@gmail.com" + + // If Blunder's playing a game with no time limit, it shouldn't spend too long searching, + // so pretend we have a constant 10 minute time limit. + DefaultTime int = 1000000 + + Banner = ` +██████╗░██╗░░░░░██╗░░░██╗███╗░░██╗██████╗░███████╗██████╗░ +██╔══██╗██║░░░░░██║░░░██║████╗░██║██╔══██╗██╔════╝██╔══██╗ +██████╦╝██║░░░░░██║░░░██║██╔██╗██║██║░░██║█████╗░░██████╔╝ +██╔══██╗██║░░░░░██║░░░██║██║╚████║██║░░██║██╔══╝░░██╔══██╗ +██████╦╝███████╗╚██████╔╝██║░╚███║██████╔╝███████╗██║░░██║ +╚═════╝░╚══════╝░╚═════╝░╚═╝░░╚══╝╚═════╝░╚══════╝╚═╝░░╚═╝ + ` +) + +// Respond to the command "uci" +func uciCommandResponse() { + fmt.Printf("\nid name %v\n", EngineName) + fmt.Printf("id author %v\n", EngineAuthor) + fmt.Printf("\noption name Hash type spin default 32 min 1 max 256\n") + fmt.Print("option name ClearHash type button\n\n") + fmt.Printf("uciok\n\n") +} + +// Respond to the command "position" +func positionCommandResponse(pos *Position, command string) { + // Load in the fen string describing the position, + // or load in the starting position. + args := strings.TrimPrefix(command, "position ") + var fenString string + if strings.HasPrefix(args, "startpos") { + args = strings.TrimPrefix(args, "startpos ") + fenString = FENStartPosition + } else if strings.HasPrefix(args, "fen") { + args = strings.TrimPrefix(args, "fen ") + remaining_args := strings.Fields(args) + fenString = strings.Join(remaining_args[0:6], " ") + args = strings.Join(remaining_args[6:], " ") + } + + // Set the board to the appropriate position and make + // the moves that have occured if any to update the position. + pos.LoadFEN(fenString) + if strings.HasPrefix(args, "moves") { + args = strings.TrimPrefix(args, "moves ") + for _, moveAsString := range strings.Fields(args) { + move := moveFromCoord(pos, moveAsString) + pos.MakeMove(move) + + // Decrementing the history counter here makes + // sure that no state is saved on the position's + // history stack since this move will never be undone. + pos.StatePly-- + } + } +} + +// Respond to the command "setoption" +func setOptionCommandResponse(search *Search, command string) { + fields := strings.Fields(command) + var option, value string + parsingWhat := "" + + for _, field := range fields { + if field == "name" { + parsingWhat = "name" + } else if field == "value" { + parsingWhat = "value" + } else if parsingWhat == "name" { + option += field + " " + } else if parsingWhat == "value" { + value += field + " " + } + } + + option = strings.TrimSuffix(option, " ") + value = strings.TrimSuffix(value, " ") + + switch option { + case "Hash": + size, err := strconv.Atoi(value) + if err != nil { + search.TransTable.Unitialize() + search.TransTable.Resize(uint64(size)) + } + case "Clear Hash": + search.TransTable.Clear() + } +} + +// Respond to the command "go" +func goCommandResponse(search *Search, command string) { + command = strings.TrimPrefix(command, "go ") + fields := strings.Fields(command) + + colorPrefix := "b" + if search.Pos.SideToMove == White { + colorPrefix = "w" + } + + // Parse the time left, increment, and moves to go from the command parameters. + timeLeft, increment, movesToGo := DefaultTime, 0, 0 + for index, field := range fields { + if strings.HasPrefix(field, colorPrefix) { + if strings.HasSuffix(field, "time") { + timeLeft, _ = strconv.Atoi(fields[index+1]) + } else if strings.HasSuffix(field, "inc") { + increment, _ = strconv.Atoi(fields[index+1]) + } + } else if field == "movestogo" { + movesToGo, _ = strconv.Atoi(fields[index+1]) + } + + } + + // Setup the timer with the go command time control information. + search.Timer.TimeLeft = int64(timeLeft) + search.Timer.Increment = int64(increment) + search.Timer.MovesToGo = int64(movesToGo) + + // Report the best move found by the engine to the GUI. + bestMove := search.Search() + fmt.Printf("bestmove %v\n", bestMove) +} + +func quitCommandResponse(search *Search) { + search.TransTable.Unitialize() +} + +func printCommandResponse() { + // print internal engine info +} + +func UCILoop() { + reader := bufio.NewReader(os.Stdin) + var search Search + + fmt.Println(Banner) + fmt.Println("Author:", EngineAuthor) + fmt.Println("Engine:", EngineName) + fmt.Println("Email:", EngineEmail) + fmt.Printf("Hash size: %d MB\n\n", DefaultTTSize) + + search.TransTable.Resize(DefaultTTSize) + + for { + command, _ := reader.ReadString('\n') + command = strings.Replace(command, "\r\n", "\n", -1) + + if command == "uci\n" { + uciCommandResponse() + } else if command == "isready\n" { + fmt.Printf("readyok\n") + } else if strings.HasPrefix(command, "setoption") { + setOptionCommandResponse(&search, command) + } else if strings.HasPrefix(command, "ucinewgame") { + search.TransTable.Clear() + } else if strings.HasPrefix(command, "position") { + positionCommandResponse(&search.Pos, command) + } else if strings.HasPrefix(command, "go") { + goCommandResponse(&search, command) + } else if strings.HasPrefix(command, "stop") { + // TODO: stop the search of the engine + } else if command == "quit\n" { + quitCommandResponse(&search) + break + } else if command == "print\n" { + printCommandResponse() + } + } +} diff --git a/engine/utils.go b/engine/utils.go new file mode 100644 index 0000000..48e33b8 --- /dev/null +++ b/engine/utils.go @@ -0,0 +1,36 @@ +package engine + +// utils.go contains various utility functions used throughout the engine. + +// Convert a string board coordinate to its position +// number. +func coordinateToPos(coordinate string) uint8 { + file := coordinate[0] - 'a' + rank := int(coordinate[1]-'0') - 1 + return uint8(rank*8 + int(file)) +} + +// Convert a position number to a string board coordinate. +func posToCoordinate(pos uint8) string { + file := FileOf(pos) + rank := RankOf(pos) + return string(rune('a'+file)) + string(rune('0'+rank+1)) +} + +// Given a board square, return it's file. +func FileOf(sq uint8) uint8 { + return sq % 8 +} + +// Given a board square, return it's rank. +func RankOf(sq uint8) uint8 { + return sq / 8 +} + +// Get the absolute value of a number. +func abs(n int16) int16 { + if n < 0 { + return -n + } + return n +} diff --git a/engine/zobrist.go b/engine/zobrist.go new file mode 100644 index 0000000..0a736d7 --- /dev/null +++ b/engine/zobrist.go @@ -0,0 +1,153 @@ +package engine + +// zobrist.go contains an interface for creating and incrementally updating +// the zobrist hash value of a given position. +// +// https://www.chessprogramming.org/Zobrist_Hashing + +const ( + // A constant representing the value to use when seeding the random + // numbers generated for zobrist hashing. + ZobristSeedValue = 1 + + // A constant which represents when there is no ep square. This value indexes + // into _Zobrist.epFileRand64 to return a 0, which will not affect the zobrist + // hash. + NoEPFile = 8 +) + +// An implementation of a xorshift pseudo-random number +// generator for 64 bit numbers, based on the implementation +// by Stockfish: +// +// https://github.com/official-stockfish/Stockfish/blob/master/src/misc.h#L146 +// +type PseduoRandomGenerator struct { + state uint64 +} + +// Seed the generator. +func (prng *PseduoRandomGenerator) Seed(seed uint64) { + prng.state = seed +} + +// Generator a random 64 bit number. +func (prng *PseduoRandomGenerator) Random64() uint64 { + prng.state ^= prng.state >> 12 + prng.state ^= prng.state << 25 + prng.state ^= prng.state >> 27 + return prng.state * 2685821657736338717 +} + +// A constant which will be a singleton of the _Zobrist struct below, +// since only one instance is ever needed. +var Zobrist _Zobrist + +// A struct which holds the random numbers for the zobrist hashing, and +// has methods to create and incrementally update the hashs. +type _Zobrist struct { + // Each aspect of the board needs to be a given a unique random 64-bit number + // that will be xor-ed together with other unique random numbers from the positions + // other aspects to create a unique zobrist hash. + // + // Blunder uses 794 unique random numbers: + // * 12x64 for each type of piece on each possible square. + // * 8 for each possible en passant file + // * 16 for each possible permutation of the castling right bits. + // * 1 for when it's white to move + + pieceSqRand64 [768]uint64 + epFileRand64 [9]uint64 + castlingRightsRand64 [16]uint64 + sideToMoveRand64 uint64 +} + +// Populate the zobrist arrays with random 64-bit numbers. +func (zobrist *_Zobrist) init() { + var prng PseduoRandomGenerator + prng.Seed(ZobristSeedValue) + + for index := 0; index < 768; index++ { + zobrist.pieceSqRand64[index] = prng.Random64() + } + + for index := 0; index < 8; index++ { + zobrist.epFileRand64[index] = prng.Random64() + } + + zobrist.epFileRand64[NoEPFile] = 0 + + for index := 0; index < 16; index++ { + zobrist.castlingRightsRand64[index] = prng.Random64() + } + + zobrist.sideToMoveRand64 = prng.Random64() +} + +// Get the unique random number corresponding to the piece type, piece color, and square +// given. +func (zobrist *_Zobrist) PieceNumber(pieceType, pieceColor uint8, sq uint8) uint64 { + return zobrist.pieceSqRand64[(uint16(pieceType)*2+uint16(pieceColor))*64+uint16(sq)] +} + +// Get the unique random number corresponding to the en passant square +// given. +func (zobrist *_Zobrist) EPNumber(epSq uint8) uint64 { + return zobrist.epFileRand64[fileOfEP(epSq)] +} + +// Get the unique random number corresponding to castling bits permutation +// given. +func (zobrist *_Zobrist) CastlingNumber(castlingRights uint8) uint64 { + return zobrist.castlingRightsRand64[castlingRights] +} + +// Get the unique random number corresponding to the side to move given. +func (zobrist *_Zobrist) SideToMoveNumber(sideToMove uint8) uint64 { + return zobrist.sideToMoveRand64 +} + +// Generate a zobrist hash from scratch for the given position. +// Useful for creating hashs when loading in FEN strings and +// debugging zobrist hashing itself. +func (zobrist *_Zobrist) GenHash(pos *Position) (hash uint64) { + for sq := 0; sq < 64; sq++ { + piece := pos.Squares[sq] + if piece.Type != NoType { + hash ^= zobrist.PieceNumber(piece.Type, piece.Color, uint8(sq)) + } + } + + hash ^= zobrist.EPNumber(pos.EPSq) + hash ^= zobrist.CastlingNumber(pos.CastlingRights) + + if pos.SideToMove == White { + hash ^= zobrist.SideToMoveNumber(pos.SideToMove) + } + + return hash +} + +// Precomputing all possible en passant file numbers +// is much more efficent for Blunder than calculating +// them on the fly. +var PossibleEPFiles [65]uint8 = [65]uint8{ + 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, + 8, +} + +func fileOfEP(sq uint8) uint8 { + return PossibleEPFiles[sq] +} + +func init() { + Zobrist = _Zobrist{} + Zobrist.init() +} diff --git a/engine/magic.go b/extra/magicgen.go similarity index 90% rename from engine/magic.go rename to extra/magicgen.go index ba31dcd..622c4ad 100644 --- a/engine/magic.go +++ b/extra/magicgen.go @@ -1,30 +1,24 @@ -package engine +package extra import ( + "blunder/engine" "fmt" ) /* - The file containg the code for generating Blunder's magic bitboard numbers. - Magic numbers are essentially randomly generated numbers that happen to allow us to create a hash function to index a table of pre-computed moves for bishops and rooks. - The algorithm behind finding and using magic numbers is as follows: - For each square on the board, all the possible squares that could be occupied by a blocker, whether friendly or not, are generated, as a bitboard. This bitboard is usually called a blocker mask. - The blocker mask doesn't include moves on the edge squares of the board, since even if there is a blocker at the edge of the board, the sliders will have to stop there anyway. So a blocker being there is irrelevant. - So let's say for example that we're considering a rook sitting on D3. Here is what the blocker mask would look like for that rook. - 8 | . . . . . . . . 7 | . . . 1 . . . . 6 | . . . 1 . . . . @@ -35,15 +29,12 @@ the blocker mask would look like for that rook. 1 | . . . . . . . . ---------------- a b c d e f g h - Next, every permutation of the blocker mask is generated. The number of possible permutations is 2^n, where n is the number of bits that can be set in the blocker mask (in our example, n=12, so 4096 possibe permutations). - Lastly, the actually moves a slider has needs to be generated, given a square to move from, and a blocker mask permutation to describe what squares are blocked. Going back to our example, one possible permutation for our blocker mask would be: - 8 | . . . . . . . . 7 | . . . . . . . . 6 | . . . 1 . . . . @@ -54,9 +45,7 @@ example, one possible permutation for our blocker mask would be: 1 | . . . . . . . . ---------------- a b c d e f g h - And the resulting bitboard for all the moves a rook would have would be: - 8 | . . . . . . . . 7 | . . . . . . . . 6 | . . . . . . . . @@ -67,47 +56,53 @@ And the resulting bitboard for all the moves a rook would have would be: 1 | . . . . . . . . ---------------- a b c d e f g h - - Once we have a blocker mask and all of its permutations, and for each permutation we can generated the moves a slider has, magic numbers can be found by generating random 64 bit numbers for each square, and testing if they're magic. - To test if a random number is magic, an index must first be generated using the formula - index = (blocker_mask_permutation * magic) >> (64 - n) - where n is the number of bits that can be set in a blocker mask for a square. - Once we have an index, we'll know the random number is magic because the index will be unique (i.e. not generated before by using the same magic number in the above formula with a different blocker mask permutation). - We care about the index being unique, because the index is used to map *each* permutation, to a moves bitboard. This way, possible moves for sliders can quickly be found by getting a sliders blocker board permutation, given the current state of the board, and using this permutation in the same above formula to generate a unique index that maps the permutation to its specfic moves bitboard, for that slider, on that square. - However, a random number can actually still be magic even if it is creates an index collision between two, or more, blocker mask permutations. But what must be the case if a collision occurs is that all of the different blocker mask permutations that the collision occurs between must all have the same moves bitboard. - So for every square, and random number is generated, and tested using the above two conditions. If it passes both conditions, it's a magic number. If it doesn't, then it's not, and we need to generate a new random number and start the process over. This is done repeadtly until we have magic numbers for every square, for rooks and bishops. - These numbers only need to be generated once, and can then be saved into an array and used in the program. So magic.go isn't actually used into releases of Blunder, but serves as a didatic example of how to construct a program to generate magic numbers. */ +type Bitboard = engine.Bitboard + const EmptyEntry Bitboard = 0 var MagicSeeds [8]uint64 = [8]uint64{728, 10316, 55013, 32803, 12281, 15100, 16645, 255} +// A struct to hold information regarding a magic number +// for a rook or bishop on a particular square. +type Magic struct { + MagicNo uint64 + Mask Bitboard + Shift uint8 +} + +var RookMagics [64]Magic +var BishopMagics [64]Magic + +var RookAttacks [64][4096]Bitboard +var BishopAttacks [64][512]Bitboard + // An implementation of a xorshift pseudo-random number // generator for 64 bit numbers, based on the implementation // by Stockfish. @@ -143,12 +138,12 @@ func genRookMagics() { for sq = 0; sq < 64; sq++ { magic := &RookMagics[sq] - magic.Mask = genRookMasks(sq) + magic.Mask = engine.GenRookMasks(sq) no_bits := magic.Mask.CountBits() - magic.Shift = 64 - no_bits + magic.Shift = uint8(64 - no_bits) permutations := make([]Bitboard, 1<> magic.Shift - attacks := genRookAttacks(sq, permutations[idx]) + attacks := engine.GenRookAttacks(sq, permutations[idx]) if RookAttacks[sq][index] != EmptyEntry && RookAttacks[sq][index] != attacks { searching = true @@ -192,9 +187,9 @@ func genBishopMagics() { for sq = 0; sq < 64; sq++ { magic := &BishopMagics[sq] - magic.Mask = genBishopMasks(sq) + magic.Mask = engine.GenBishopMasks(sq) no_bits := magic.Mask.CountBits() - magic.Shift = 64 - no_bits + magic.Shift = uint8(64 - no_bits) permutations := make([]Bitboard, 1<> magic.Shift - attacks := genBishopAttacks(sq, permutations[idx]) + attacks := engine.GenBishopAttacks(sq, permutations[idx]) if BishopAttacks[sq][index] != EmptyEntry && BishopAttacks[sq][index] != attacks { searching = true @@ -233,7 +228,7 @@ func genBishopMagics() { } } -func genMagics() { +func GenMagics() { fmt.Println("Generating rook magic numbers...") genRookMagics() fmt.Print("\nGenerating bishop magic numbers...\n") diff --git a/tests/perftsuite.txt b/testdata/perftsuite.epd similarity index 100% rename from tests/perftsuite.txt rename to testdata/perftsuite.epd diff --git a/tests/polyglot_test_files/test1.bin b/tests/polyglot_test_files/test1.bin deleted file mode 100644 index c3cbc8459b46002a5c0b4fdb7e86fcd9dee891e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1344 zcmXxkX;4#F6bJA;LS7QGytGjQ5i)GbphOhvkPOxTHHJdf5=V3(U@P*_G1u3*_7O_$wv@_@Re*4Y;emM7@b1%ZB zsx4Y=G%V?RJ#4*Cadm^em+$&e~L@#h`yXk8}j`%j(0*+&$R&}1>N4MwTcRTtr}z!d~NTeYr^X1E*{~nbqj? zQ75$lEM{Jt3y8`>nGxW$r4q>zKk05#7Wlx%nokN#ZmuwX2WM6E-&|T4Zf5v{kD(>= zdA^o&2#T9qUbQ3md@nf{EGrWp{*rgPj&=}yqIIq}`_P$C6t7P{dX32`E;1l^|K#_F z7i*jMbBK>%e}VsecC7821v-8|3OiO+u`DSd#6W&wXI$Ic3_R(10fVp%d2@&Qt|?o%k$|74<*;D$}n zjq$`IxE<;0UN_4*Dn6gBeZ1De)R{zv4D#0H$n;V_T^V)@+!pmGJ2`>ujyZA12kFD# z_8q11z#Z(0>+-&cE<}Yp_nE*HKJc1y4E()lTWCmZLJLI$zGGoZ^M!wzX?VXa5n4Cj zsJxw|49G1>;v4-U?^jel*otAsz)RhBvJrewG1{XEi@5KC&)+@O7s$J9=5~|?`2!VY z|DJY!096mR`6#B*Hy_tC@p^3U~=o@9ML{SU|| zW`1cDUmUU0*1(e!S=ToxKP#k9gQtQ>^17>Dzy1GTO?z$IzW%nAMfZVxcIdIm&dD=T zrob;oI{K!t${OS?@M6+eHdbb_1H<3L->ypMYEe-l#RKx!|9m%9^&J2{EfG@EFDgno&ga|n-|OC==k9r)_dRr%M4{X9Xw`y`2nqSO z@a$*45l;L8Bn`Z%Kqu%{+#EDt3|<_}DaSVGR+DqVON!cW8@Y@wsu*nR7IYCaY(sG& zcgrk}*QU}p=>rp;?CQ6`J<$`3umI|LW8S;7UPyqe29|IpQ7 zS4E!D&FWq{Y76GRsuMDNmVH74!5*x#u`~B*H)!9$LVN!L#!ogM>jitsrnh{Kt=3>g zU>~QH4SEs>!<`AM+-1ty$6iPq*v})+)+TfII*GV%KvtVDLoRb=RzSWrbyAU;;FHfh z2o5~pq`O;R9YVVU4z9Y^$R0hc#7~1mL<1h7$%jN#BRGuek0qYUxJkDLhaU>;nQS#K z!Q;VEGo@oP+pdcFKF)+?v|MenR@2%ckKdSE*mPrkE}6Jrf;e0<^`)qpLG+pQNh&$q z|67jAVLs)t##{f5Qv6T<`@Zl)(qYrWi?&dL6?i1xR38?~R1p@i(h|nGeHQlM%-BHf z*CC^pv>%+)dCbnkYGnqQ0xsNlb|g_&Sc2ar{LsnRAIsMhuQTpRcr0#{ zpCO#b(11%F<&q)h=QxDuQ?+(!(n)K}T~y-z)iKH#&FeufMFR6$ub`O`wkjX52Uj_I z9xeBun>G&vKNK+fW;lIwSR~>7E$Ye#C$h~~fOVRxc& zl&_Z_FR4I&fS)=HAKUY^q`|Bk{0v=nE8WjGl(q}pShd~GkZ#(DUIVw3NXCj#CIfmleU5X0^JiybpQYW diff --git a/tests/polyglot_test_files/test11.bin b/tests/polyglot_test_files/test11.bin deleted file mode 100644 index 682758db9fd450d0ade88d1d0d414712454e890a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 288 zcmZQtf6wB7*m7>(bqtIQK)`p>_d>;|ufI4VA$)%SpY9c{J#l=mA$+mmzbBn%=6JBH zK=^Lf($Qn{yO$2nV+g;= zW_G||LC0ylsSy6$<2Cl`X-en$*FyM<7TL;Owr-rx@EpS5y7IJf_fE}UJc$tg;fdFd zb8cKQo$EA&e@xTkRM(v3NJbwB|LWR>Y62otjx$b$@E^+-eDX-iyTb-`-)rgKNcAlI)8(2N2&1t)ogB;!ltaaZeQIVISOvnanyG?d__tte&gOM*seG0X+oWjbH zN6#%ciSl{0Rj^LN*&B(;=N-fpz((;N`SRY@hp9gym*#$*lisFF+5|RMw0CF|9mELv z$Q@DzPNf`0f+X0K|HSg3+gJgn0XDZ3XgLq7l@SZU7QwR9adotBq%JVa)2X&|TF;E| z4LL4Pw_d>dL=FL4rEX7<^5(j&@0Ybv&?V*Xe>;-V_pNgd{m?BMHp8S~e$Qe3aTiB> z3egK}Lr&e^8W}r4`5n2ZQR}&Zwk)*_Y-i%bveu7VqWFRBcjYR`UHNg7@CNKqdHGkr z#pO^j9GJsuG`Sh;luSDZb~oEGb<*zb5^WUOCrI+FY5WbQG1&9`=V)ODL5vy>_Oc9C zcFJyd7gt7p;i$yi;G2y@>#={&m;F*}4Y|~fknb;BJy~e(?)!1q&TY|0L0wt5h*gD)})DyF29!kG=oCKNRbHCbcE7jUY^bEf64YD4m6aHgNa z1DEmmtzzhY$WkV}%?K+qp*ujHcbXF}v@E@iNAr1t%ARsYk|tdT<$`LLzU%2vn0(|> zRtC=XlO9zQe8X{4$IeBTiJ|@!)73|X{{tJ=MWO05C%Jow(-g)VL1L3KRp zTa#W*2LC~th>ZH)#M!oU!L@vig!ZfXU0h1T&VXK~FTCI4@j?rsNUu4`YE+P|Cz)!bo zEg1O5mDA6FdtHL9mN&4)7&G8!Z1drM?P`C*b8w%}OZzvHYgwdpaDS)y;D!8=d2w|A zzDzv+xn5l*O$_z><-3f%RVA7G@HsI5%5oFVJ`O}9Y9izNlK61I!;Hc0nzwil;3TH!knumm};CNay=yanN`R4i3cRM>FSf8zAR*DPM-_+nvUa zf)@|pOpbDKn<1h3tx5{#-wo}$L;L}`dB#cpNF?D#)c^}pzb`3?B4=P7V9Vq{eJ^&( z6gviNHU4(kJFB*YjQY0;;^-MV71@!|`r2HYbI)W=k0sLx>TQ$jclS)`aOvB?c3*FrGUmAB#rjDgT{U&Os&j+bd00Z^6X{Dy4=ah)f429f-JT(%w`Asp+ z6WMix^XO_-CCLH2<#=Xqic?`Z=@{56;EWb8H(iU4`tx>JT0UfBe}sbO=lg2N^>LtY zuv!Sz@7lwP=MQY?BoyFaUV^0VKCOYuLzZ7LtobSI4jt`>(9||uU3H?B69&0ZRF`;Q zP}!oU0}i{WNW9^As*LjvyuWFo5r25@1g8ib%`d!d6c=(E-vN#j5TYl3ohF=m6 zm)o$VY6alJUIN_zgQ;AP=2ae8rnst1Ic3P8wGYR<- zPE9%8r=y?s7JO8{7}L(@Ze#|7)5jc(0?syUA&bBn{?uCuEKWDp3O-dgE!f|+eIqp& zoEiHp#!6<&WR!z*4i`l8?b{VrQ;j#`3|nZ7l0E>jL@7ee2 zS@wGMt%9mG>^K7kT3VN@`QvaQC7}?dz{Ra7X_&aGJTfdL$2v(JCYm^DiI%o@!?+O3% zt!i7xjRjBI$$INVRmM0vAD;)`Sd=fFka2Ax|N6A+Zj`s^C=2c9w~)?dKj>`@!qNSo Zc4wuA9^fsR-QVe01L0HWj?&rt`yb@G=bHck diff --git a/tests/polyglot_test_files/test4.bin b/tests/polyglot_test_files/test4.bin deleted file mode 100644 index 9f06dbbf8b15e2aa94c3908d10abcb6d949174a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1216 zcmXxjc}$aM9Ki9n<)yy7?c29tJZ9MfSP;AdvLTFd6bYypX`qXMG|FK&B_O7_RkxyG zRbq`1Y`u!2NPu@7n-Q^vDherz)?*+;QnXVX6j((-WNsWY`#pR9`{bLvd5+(UCr+7N z#uH4~I*f&VT*NXXiTfKh{1W8oQ%iE*885MPVBUsD6`!4)abqWgDczvrSF2?JyAP%{ z%M@Y!A&OoOo*yoM<2#^v!g2zCl6`eXi|6*Zl!Ar%^L-+2*96n)V6P6f!K%*6!wIn8 z{k;-)0X{?pf>)hz)Yt5&bMQRCf&DHe8yg)DoO-}tOeC2t_J#wz5%Aiq@aq=SVG(}< z91{21yKEHqpd-Lx*Y+JrXtKKDeaM5tkTHkw8F3lB?zbC}%z3FN*#M3SJ-v8g@pP$^ zJ96x)sP^~DrR>GXPs`=Z@#XJX(cswdGS_~0n?GwFc*Cxcwz=-9LQXMQCVRcDT9J9l z*$ez#r}irDI?_R){U{$+@fRIZU1X&|R|#i##&bP~d9T0+bNpWBO;pLL)!>4TRYfgp zTFulbxbQ2zWX4r|ni3)B=aXmq5?lFSg4Ij?QgxFQhui}`x~=Q+5ym1R`@x#&fp}p= zWcEk>Hy_&Imb_Nmh!I2gdA{=N>A_O!C^r?lev4jEq@-)uUEqt6vx=rG<4NrLo;1#-P{puc!6KC8x2Z;A{KECsRiIe7Q@&jTu{1ljGv%|MzE* zE)=Tqgnm2&y6Lzsr;U1B$h`@^mBckXv~NAXh71IiBEq4FbDQ`nxz1 z-JfOPghgctQbu9WJAc{xl1cZqY}yCz@;LA($L-k#KKfo)!_-{T zW^Rcf}|*_?9o-m=Vr$cL?I zm0f4U5zMw2!jEW^yTJ2F#*aH0!jC?GyL&p{VJ~iF2tV=u;d7Pc^Hdq8K=>()^N-KH zu&;ydF_eFs$-j3sb0a%ceYP)WtoE@9JiIy(`JyK?0}{hlr}5iD_zj!31W1cdW8jCn zznO36CiaMn_xRUB!talm9sPzi+==rYgg^an&40Np`5c}v5dQ2g zrXPQ8#2y0^7dU+_UhaN-ot$Jf-)jhe`K+s#lAEjAdGjFrwe~(KFaWI6=lZ!*b$ zxUls8afU_+fAgYucg`AIPiB1v;qN&(`Hf+(+G7T2`q^9VXfa7?B@1&XME;1!OM~w> zMc#7%h47DOq|Dtc&M3qz1LZFY))d}w@B$aqKNpk#RWuod_5;%!*u9sJE$1|P;a186 z4bK}7dfiton&81S3nG7y^Uf5OZyHy5G$8y(vgy44-!{+Vf~Jp`T2)d{qvmw-{DH{7 zKDq4f>a1EX&U+C4_jglE8h*YkV!jXI|IkQN+-2kTnQH-r|HpV)L70ESHpb}?J^<1> B3~K-Y diff --git a/tests/polyglot_test_files/test6.bin b/tests/polyglot_test_files/test6.bin deleted file mode 100644 index 81cc8b45dc5e5eb9d5669a46302ac6ee6afec63f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1168 zcmY+?YfMsM7zglk4k9;!yrf;Uv=VD9$aQg3Hk4Ufm}NzxYo0$J9%F37 zaP04)hL_CQ_w9Z)t_IIIGIQ4*yj#%4FlW`xF7LL)RFVW7;E@#1o4?^LaSqwr8m^RfM>~yymo}fW+1KcT*g`O` z?Tt|;>-3-xA@`g1ItB&))bn6|)!C5zsNgy5Oz`rqSfxQQScW-)L!WT(o(viDBaeXB zgp|(mEOuNZc_VAIOirqBg6#p0kXF~{{~TrD3~*%L+NoytnORgDSnPhd=786jkIQ0k z4CjYiUV25kGkRal^K#>qA~fE`0{JF^y)IyXQ4)^s+x#hQ+(#WzkF`NAk>#$I%*&Ru zLtviL1v;5(vPH=nggkL3e|Ur^^r4{l?NL89Qvzc+WEtd&CBGXRs9U~bg~%2rdBxUS z{nOt|e|DMWm7}V{(LPjIaZU2@hF)h^=w}^ze^VhbB@;c!wl2>B({UMb1Dwro)vi^T zBWY+qM=iQOZnS12(Fyr6=8pnnYV$dlE69S1jgzinrOZX(g2(q?wtL%JD1FFNzHuqN zqbc}l@Tm)>lmpLxUB{!rr!!A<*GGQe%T5EA*MApgSmGRbII=ZwlRC)r5mgGln5IuG ze3;RJqxn=~1a;m-Qy&fOzgivJBpRp`p5Sm#fa zaGtI_PAUc0jXZHp3R8S$Uk6{6+j48m5)xPraMQu$B90+}PrL;`5X%EoZWrsBXkP8y z&~C`I}EH$%0h6jD7Z_LDB8{Bg3X2L^ytvpu@p~mpZ~GPL-b-J(fYrg z)`17?LqDrux8I{*0uM_I$R+aRFC_GT*)B11azxt?PVax5HfilN@KiVj`s2!56^%Vn o8H}A^dqKJ1{Lt8aXK{f5sdEtH?gMmyfj<(vj6I4Y>6Io0*^uY^V-gw<7GZuqw zWr?v|jc^al3T#(%b%IM>7fMJ6uh(;Hv+Fq5MAig5_+=<_RA$amT#zH8-ew<`vDy2< zj%y{_3$y}wUQTlCl}7)Rsto!lP9+wvd^ zKLEL(i%Y(&YlSbR19^n#di$Z)N=hbp)3E8rw1;dSvl1Lo+_NEmFq6po2;NqdaKyrX+Aad!jt)%^N|XcbrQHwC*MgMR9f)LtQjH9Od=IY^nW-oU8|q!_9LpSvOxM;>dE}Sfjq0eJW^u6Rs>jPi;u05j-JJ ztgg_hiSJ#b@)(?Ax3+n&6X_128+?dWf06#vy(et6Ua4m8C7oe7gDQnQZJ;>l;AqPf zdkTD*YoXB`KpSL7Bi|pn9zK*djlTtprfDYxTX+lcQgDWvWK(>P+Hnk>*P~|5PsiA4 zyQm(J9}`-psBOo|v^;Q5>0F82DR3Lh82Rt-XYt8$Hv$?@u7mP!ASL!P9uK*=vmwaJ zZOV}z0+x9FaZ$tXK@YtIT%h#O(kEQFOhRUkOejh9hTw^Cqs90j4&)f-?#V3#6RJ}Dl zKVRAC%w?3i}+6%^ZnPQ%(7ENbwbr3s9*om;DVW1 zU_KSiuYvGJ=cK{(cNBHV8wqUz3F=v)qz>d2GaVOZBW9^u$munnwcVlP{bwaei)qy#n{P>J$(6o8+mW^Vhet z>(C7^Mhm4C^8TTtPUWYMJCnZdz?U*$xci02`pCN^Kd@YFX2^T&Q33(IkMi!ArN8pq zt7xYoADvKygb;SUp1-eSME}mJx<}`*eUOjMuJifkQg}9P5BTMwBLdH~%|7%0gSTdw zF!+Q)5DV3x5^m)XofK!7Xg{YrDh`W7*>NQFetlZ(>eORce}y$gAblP0r!K5GFRYb= o4MY3N0%s2_=t?gO^*5uaGZqx+zZ|IEIBR^xTb0cIiiU;d*UGynhq diff --git a/tests/polyglot_test_files/test8.bin b/tests/polyglot_test_files/test8.bin deleted file mode 100644 index ee890f92323ce656a806f6d42207c3cf2f664f8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2064 zcmZA2c{o&i7zgm#w=*-RN1I~gw%1kaD#Zvj(oL7LOp>T+(jv=sMNi1M_O3x?Niicy z+9zbneJl@^Jz47ZxQeWyDAyW>yS%^o=bryQ&-Xp&{LXvM?>&T#%}yTK3+w3`1R~+z zBfQ|RaCwS1>nfPSQiWkcOhu{&(+anU-Xt~_aC5*6qI-n-qbhx>9WssmSY=-w#N{K` z;Oj<~ZK7bC{$~T^dinRcH{`a=hhU~)`GGBC`OfT5$ihIMrAi`=)PnqS^N-9r^ZgtT zFs3+c{M0yK$U*0`Lk?UVZ!E85u7{ixtRdqq5;u^A$lbOAVWV<}eF}`f^j_g+cxWY~ z3%sD^6=UjklM(wIc=4GKo=KmBGbskFRnireo78Ze*$w9D2{I%*g#Dai0_orO@vbuD zy}Sok@H3F>*4XWFGzhC_YJXRex5%A!ZKc)yF-#fqw_Ky{xM@Se zEwJHez`on*eH~mMFyA=FQRX|piyHto`AEJfIwGrOJO-P__iz-?``6GugUv){e>(7! zYS~lJKeI6VsCDPGWtbB39c9+CBYPhI&YcEZ@{=_eT$)ZJT?6l$=v2hJ?wrF$&$rtu zF7y4(sA@Xu=TGLM;)H9;%h)lfw{8kBaJ?JuL`eYem89KUcAJ=t9|b${;ylg$ct#xb zy*LwFBdj*mMzU=ncd^+vV38E6qSb&!K4EDcZ*u(bZOAGG9uiWF(U*gdCKR%Ir#<$N zPCoeD(vdGSfpSYO+J6B9t>c=r`up$)ke}DJcvqk0FwQ)W{6QnGuGRG|)d3v5 zaX_-zs$`6U_N&A;$KzwfUGPDUp@4wJ{iep_^QAI%W`FZ|>M*g&^0gm?93;QgqD+4Y23Fv#SFrbwW z9>B{uMR2_e6LC+h=BPOd^}8Z9=!G;d@jVHxr&^O~B9uF(a`d3SW_ePFu}S<03+?aP zBMvPF5D&+OLPi&M0j#|u>;6@2{dXzWPhx>z@g81uu3Y)dn|Cgp)RF(4UtiSqx zugzsTMPs{4St6)^!*?p%MR#4pLeJmg5oK{Rb=H+SAM$tB*`7tQl3ZdOxOHersl&jb za58$o?OW;fZA&XgiHjiblu~@wobX*sN8fXoXr4?V5I7J8koQb9+?f+GGoL#Q?mL)z zYRsvlm5%0T-}7*nm%hAc?l;H>tG=xbSbVnxHv5smZq>tsg&CcV@Vj7Es{bq+TC-WU+;gP^L^ew-uHc;7f~-XVy6nlBzXjf{J-#l zVbMZ-j}a>lj0Wl`r*h|5GMSh~7A~ghqG+-hOrr*vMJlX{2yC#d+S+z#qslvS9a!!` zkVNe8*oDjm%X_(Z2iJ|c<7L5YAx$^4BAHLG0`IlunjGkwTqFMj{=!pHYP@2*hxys9 z(=Kmw8dPzGkZWiJ9}kd|t}s7=b=wmtKMa#@Q&hov4%u04X+|BabC}EG8edc6qwyJF zgY>(3Pe|xfnm^ctb0%DK57n672sWF(#__&8PQ_V)kH;ehwY@t+N(kme(qT18xIL~7 zY~{8qhsAFFM2P~QG%wnvm?!1oCon(UTVK4xMi{kVJKc%u;Xe{2#Ft>lx1^{|_t4*I zn&4BNi(Hm<^M3pY=CR@DSKF%!P%p6SU-IXdn#@l#z685BiRRNT9q`3jgFO~(=2OfR zvMAnQj`CHDxN9{txJ0mz9^zF_Nvx770sHkQ#-aS5iy2Lrx1D%K4)a3B0{BAxlj@rL z`BgM6%&Q;&jTv!NBIJMrQ^f`QR#(4aoCgO>&jmXDohOT6=M9csMDgPO=(Ctp-O` za4+>BNTIo;Z0PKOhmByOSv;M$+aJym@SZB zj(&bmVN;Dn5P_4Je>Rgm2B%oq{Uj3|j5~yVszhu)lAZg7@txu$#6hS}_U8`-P{m9c z9yoRXLHRyk;tOUZINdYGbSc6_5xD}+2>9@7!K%Zbgxzmuk%Az-ROwd)4|(?W%9QXN z!wr@NxUgFP$FCQ&Mp+lZ#jauQq7yH+a4FzY!~7c|3v<6vo`K8gxlgSf2em0C;Bvu| zVU_yvMS428!dc~}k5u1}s0XgJG;8rZ^ER=iQ+Cf!p35Gf#}(y~8vIxA)5Mv-AS)(U-xU5wYHvb@?d4 z4SdJ)z@s6Zju!fBu)uFi&e&4Gpke*hZQN1JQ9=ArY~Js$XVgz6=tfhp_vt&7w)w$j zpoiKC`yZv4oG9JNcS5l94%G>k^IR9pktE26B$KgjT`xz_a`5nQw^MJ!w?Rk`ctlIH zmF`B9AP>N!&gUM_740xT_s3`#FGPJN;v$O8i>RlhqL=m+$`U}mXps2bg?&c~2rA&Q z=`5T2s$5%q8F)Ow!ejnegQW~M-{XA&3mTHMN)&y_#Ut7HTTd$9G5o<(PdD^fXL$B_ zPw-3ewB%@@*: Run perft up to -- fen : Load a fen string given by -- print: Display the current board state -- help: Display this help message -- ptest: Run the perft tests for Blunder -- ztest: Run the zobrist hash tests (resets board to starting position) -- quit: Quit the program -` -) - -// Run the loop for the command mode of Blunder -func CmdLoop() { - var board engine.Board - board.LoadFEN(engine.FENStartPosition) - fmt.Println(board) - - reader := bufio.NewReader(os.Stdin) - for { - fmt.Print(">> ") - command, _ := reader.ReadString('\n') - command = strings.Replace(command, "\r\n", "\n", -1) - - if command == "quit\n" { - break - } else if command == "print\n" { - fmt.Println(board) - } else if command == "help\n" { - fmt.Println(HelpMessage) - } else if command == "ptest\n" { - fmt.Println() - tests.RunPerftTests(&board) - fmt.Println() - } else if command == "ztest\n" { - fmt.Println() - board.LoadFEN(engine.FENStartPosition) - tests.RunAllZobristHashingTests(&board) - fmt.Println() - } else if strings.HasPrefix(command, "perft ") { - perftCommand(&board, command) - } else if strings.HasPrefix(command, "fen ") { - fenCommand(&board, command) - } else { - fmt.Printf("Unknown command \"%v\"\n", strings.TrimSuffix(command, "\n")) - fmt.Printf("Enter \"help\" to show available commands\n") - } - } -} - -// Run the perft command in the command line mode -func perftCommand(board *engine.Board, command string) { - command = strings.TrimPrefix(command, "perft ") - command = strings.TrimSuffix(command, "\n") - - depth, err := strconv.Atoi(command) - if err == nil { - if depth <= DepthLimit { - start := time.Now() - fmt.Println() - nodes := engine.Perft(board, depth, depth, false) - fmt.Println("\nNodes:", nodes) - elapsed := time.Since(start) - fmt.Printf("Time: %vms\n", elapsed.Milliseconds()) - fmt.Printf("Nps: %d\n", int(float64(nodes)/elapsed.Seconds())) - } else { - fmt.Printf("Depth limit for perft is %d\n", DepthLimit) - } - } else { - fmt.Println("Perft depth should be an integer") - } -} - -// Run the fen command in the command line mode -func fenCommand(board *engine.Board, command string) { - command = strings.TrimPrefix(command, "fen ") - command = strings.TrimSuffix(command, "\n") - - defer func() { - if err := recover(); err != nil { - fmt.Println("fen entered is not valid") - } - }() - board.LoadFEN(command) -} diff --git a/ui/uci.go b/ui/uci.go deleted file mode 100644 index 4e1a68f..0000000 --- a/ui/uci.go +++ /dev/null @@ -1,121 +0,0 @@ -package ui - -import ( - "blunder/ai" - "blunder/engine" - "bufio" - "fmt" - "os" - "strconv" - "strings" -) - -const ( - EngineName = "Blunder 4.0.0" - EngineAuthor = "Christian Dean" - - // If Blunder's playing a game with no time limit, it shouldn't spend too long searching, - // so pretend we have a constant 10 minute time limit. - DefaultTime int = 1000000 -) - -func uciCommandResponse() { - fmt.Printf("id name %v\n", EngineName) - fmt.Printf("id author %v\n", EngineAuthor) - fmt.Printf("uciok\n") -} - -func positionCommandResponse(board *engine.Board, command string) { - args := strings.TrimPrefix(command, "position ") - var fenString string - if strings.HasPrefix(args, "startpos") { - args = strings.TrimPrefix(args, "startpos ") - fenString = engine.FENStartPosition - } else if strings.HasPrefix(args, "fen") { - args = strings.TrimPrefix(args, "fen ") - remaining_args := strings.Fields(args) - fenString = strings.Join(remaining_args[0:6], " ") - args = strings.Join(remaining_args[6:], " ") - } - - board.LoadFEN(fenString) - if strings.HasPrefix(args, "moves") { - args = strings.TrimPrefix(args, "moves ") - for _, moveAsString := range strings.Fields(args) { - move := engine.MoveFromCoord(board, moveAsString, false) - board.DoMove(move, false) - } - } -} -func goCommandResponse(search *ai.Search, command string) { - command = strings.TrimPrefix(command, "go ") - fields := strings.Fields(command) - - colorPrefix := "b" - if search.Board.ColorToMove == engine.White { - colorPrefix = "w" - } - - timeLeft, increment := DefaultTime, 0 - for index, field := range fields { - if strings.HasPrefix(field, colorPrefix) { - if strings.HasSuffix(field, "time") { - timeLeft, _ = strconv.Atoi(fields[index+1]) - } else if strings.HasSuffix(field, "inc") { - increment, _ = strconv.Atoi(fields[index+1]) - } - } - } - - search.Timer.UpdateInternals(int64(timeLeft), int64(increment)) - bestMove := search.Search() - - if bestMove == ai.NullMove { - panic(fmt.Sprintf("nullmove encountered:\n%s", search.Board)) - } - - move := strings.Replace(fmt.Sprintf("%s", bestMove), "x", "", -1) - move = strings.Replace(move, "-", "", -1) - fmt.Printf("bestmove %v\n", move) -} - -func quitCommandResponse() { - // unitialize engine memory/threads -} - -func printCommandResponse() { - // print internal engine info -} - -func UCILoop() { - reader := bufio.NewReader(os.Stdin) - var search ai.Search - - uciCommandResponse() - - for { - command, _ := reader.ReadString('\n') - command = strings.Replace(command, "\r\n", "\n", -1) - - if command == "uci\n" { - uciCommandResponse() - } else if command == "isready\n" { - fmt.Printf("readyok\n") - } else if strings.HasPrefix(command, "setoption") { - // TODO: set internal engine options - } else if strings.HasPrefix(command, "ucinewgame") { - // TODO: prepare engine for new game - } else if strings.HasPrefix(command, "position") { - positionCommandResponse(&search.Board, command) - } else if strings.HasPrefix(command, "go") { - goCommandResponse(&search, command) - } else if strings.HasPrefix(command, "stop") { - // TODO: stop the search of the engine - } else if command == "quit\n" { - quitCommandResponse() - break - } else if command == "print\n" { - printCommandResponse() - } - } -}