diff --git a/.gitignore b/.gitignore index a2252c5..d383900 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,7 @@ nimcache/ nimblecache/ htmldocs/ bin +Chess/nimfish/nimfishpkg/resources/Pohl.epd +Chess/nimfish/nimfishpkg/resources/*.pgn # Python __pycache__ diff --git a/Chess/nimfish/nimfishpkg/board.nim b/Chess/nimfish/nimfishpkg/board.nim index fbe064f..7be0a80 100644 --- a/Chess/nimfish/nimfishpkg/board.nim +++ b/Chess/nimfish/nimfishpkg/board.nim @@ -23,6 +23,8 @@ import moves import rays import bitboards import position +import zobrist + export pieces, position, bitboards, moves, magics, rays @@ -39,11 +41,14 @@ type position*: Position # List of all previously reached positions positions*: seq[Position] + # Zobrist hash of the given position + zobristKey*: ZobristKey # A bunch of simple utility functions and forward declarations proc toFEN*(self: Chessboard): string proc updateChecksAndPins*(self: Chessboard) +proc hash*(self: Chessboard) proc newChessboard*: Chessboard = @@ -170,6 +175,7 @@ proc newChessboardFromFEN*(fen: string): Chessboard = raise newException(ValueError, "invalid FEN: too many fields in FEN string") inc(index) result.updateChecksAndPins() + result.hash() proc newDefaultChessboard*: Chessboard {.inline.} = @@ -582,3 +588,27 @@ proc toFEN*(self: Chessboard): string = result &= " " # Fullmove number result &= $self.position.fullMoveCount + + +proc hash*(self: Chessboard) = + ## Computes the zobrist hash of the current + ## position + self.zobristKey = ZobristKey(0) + + if self.position.sideToMove == Black: + self.zobristKey = self.zobristKey xor getBlackToMoveKey() + + for sq in self.getOccupancy(): + self.zobristKey = self.zobristKey xor self.getPiece(sq).getKey(sq) + + if self.position.castlingAvailability[White.int].king: + self.zobristKey = self.zobristKey xor getKingSideCastlingKey(White) + if self.position.castlingAvailability[White.int].queen: + self.zobristKey = self.zobristKey xor getQueenSideCastlingKey(White) + if self.position.castlingAvailability[Black.int].king: + self.zobristKey = self.zobristKey xor getKingSideCastlingKey(Black) + if self.position.castlingAvailability[Black.int].king: + self.zobristKey = self.zobristKey xor getQueenSideCastlingKey(Black) + + if self.position.enPassantSquare != nullSquare(): + self.zobristKey = self.zobristKey xor getEnPassantKey(fileFromSquare(self.position.enPassantSquare)) \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/eval.nim b/Chess/nimfish/nimfishpkg/eval.nim index 8bea3ea..0b4357f 100644 --- a/Chess/nimfish/nimfishpkg/eval.nim +++ b/Chess/nimfish/nimfishpkg/eval.nim @@ -196,19 +196,6 @@ proc getPieceScore*(board: Chessboard, square: Square): Score = return board.getPiece(square).kind.getPieceValue() -proc getEstimatedMoveScore*(board: Chessboard, move: Move): Score = - ## Returns an estimated static score for the move - result = Score(0) - if move.isCapture(): - # Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker - # We prioritize moves that capture the most valuable pieces, and as a - # second goal we want to use our least valuable pieces to do so (this - # is why we multiply the piece of the captured score by 100, to give - # it priority) - result = 100 * board.getPieceScore(move.targetSquare) - - board.getPieceScore(move.startSquare) - - proc getGamePhase(board: Chessboard): int = ## Computes the game phase according to ## how many pieces are left on the board diff --git a/Chess/nimfish/nimfishpkg/movegen.nim b/Chess/nimfish/nimfishpkg/movegen.nim index 70bac34..22f783e 100644 --- a/Chess/nimfish/nimfishpkg/movegen.nim +++ b/Chess/nimfish/nimfishpkg/movegen.nim @@ -424,6 +424,8 @@ proc doMove*(self: Chessboard, move: Move) = discard # Updates checks and pins for the (new) side to move self.updateChecksAndPins() + # Update zobrist key + self.hash() proc isLegal*(self: Chessboard, move: Move): bool {.inline.} = @@ -447,3 +449,4 @@ proc unmakeMove*(self: Chessboard) = ## if one exists self.position = self.positions.pop() self.update() + self.hash() diff --git a/Chess/nimfish/nimfishpkg/resources/Pohl.epd.zip b/Chess/nimfish/nimfishpkg/resources/Pohl.epd.zip new file mode 100644 index 0000000..a289292 Binary files /dev/null and b/Chess/nimfish/nimfishpkg/resources/Pohl.epd.zip differ diff --git a/Chess/nimfish/nimfishpkg/search.nim b/Chess/nimfish/nimfishpkg/search.nim index dfaa054..e73021e 100644 --- a/Chess/nimfish/nimfishpkg/search.nim +++ b/Chess/nimfish/nimfishpkg/search.nim @@ -42,13 +42,29 @@ type nodeCount: uint64 maxNodes: uint64 searchMoves: seq[Move] + previousBestMove: Move proc newSearchManager*(board: Chessboard): SearchManager = new(result) result.board = board result.bestMoveRoot = nullMove() - result.searchMoves = @[] + + +proc getEstimatedMoveScore(self: SearchManager, move: Move): Score = + ## Returns an estimated static score for the move + result = Score(0) + if self.previousBestMove != nullMove() and move == self.previousBestMove: + result = highestEval() + 1 + elif move.isCapture(): + # Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker + # We prioritize moves that capture the most valuable pieces, and as a + # second goal we want to use our least valuable pieces to do so (this + # is why we multiply the score of the captured piece by 100, to give + # it priority) + result = 100 * self.board.getPieceScore(move.targetSquare) - + self.board.getPieceScore(move.startSquare) + proc reorderMoves(self: SearchManager, moves: var MoveList) = @@ -56,7 +72,7 @@ proc reorderMoves(self: SearchManager, moves: var MoveList) = ## to place the best ones first proc orderer(a, b: Move): int {.closure.} = - return cmp(self.board.getEstimatedMoveScore(a), self.board.getEstimatedMoveScore(b)) + return cmp(self.getEstimatedMoveScore(a), self.getEstimatedMoveScore(b)) moves.data.sort(orderer, SortOrder.Descending) @@ -110,7 +126,7 @@ proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {. # Stalemate return Score(0) var bestScore = lowestEval() - var alpha = alpha + var alpha = alpha for i, move in moves: if ply == 0 and self.searchMoves.len() > 0 and move notin self.searchMoves: continue @@ -156,8 +172,15 @@ proc findBestMove*(self: SearchManager, maxSearchTime, maxDepth: int, maxNodes: maxDepth = 30 # Iterative deepening loop for i in 1..maxDepth: + # Search the previous best move first + self.previousBestMove = self.bestMoveRoot self.search(i, 0, lowestEval(), highestEval()) self.log(i) + # Since we always search the best move from the + # previous iteration, we can use partial search + # results: the engine will either not have changed + # its mind, or it will have found an even better move + # in the meantime, which we should obviously use! + result = self.bestMoveRoot if self.shouldStop(): break - result = self.bestMoveRoot diff --git a/Chess/nimfish/nimfishpkg/tui.nim b/Chess/nimfish/nimfishpkg/tui.nim index c2ba3f3..879e2e0 100644 --- a/Chess/nimfish/nimfishpkg/tui.nim +++ b/Chess/nimfish/nimfishpkg/tui.nim @@ -359,8 +359,9 @@ const HELP_TEXT = """Nimfish help menu: - pins: Print the current pin mask - checks: Print the current checks mask - skip: Swap the side to move - - uci: enter UCI mode (WIP) + - uci: enter UCI mode - quit: exit + - zobrist: Print the zobrist key for the current position """ @@ -445,6 +446,8 @@ proc commandLoop*: int = echo board.position.checkers of "quit": return 0 + of "zobrist": + echo board.zobristKey.uint64 else: echo &"Unknown command '{cmd[0]}'. Type 'help' for more information." except IOError: diff --git a/Chess/nimfish/nimfishpkg/uci.nim b/Chess/nimfish/nimfishpkg/uci.nim index 8bbf3a8..243e2e0 100644 --- a/Chess/nimfish/nimfishpkg/uci.nim +++ b/Chess/nimfish/nimfishpkg/uci.nim @@ -37,6 +37,7 @@ type Quit, Debug, Position, + SetOption, Go, Stop diff --git a/Chess/nimfish/nimfishpkg/zobrist.nim b/Chess/nimfish/nimfishpkg/zobrist.nim new file mode 100644 index 0000000..ef21c38 --- /dev/null +++ b/Chess/nimfish/nimfishpkg/zobrist.nim @@ -0,0 +1,83 @@ +# Copyright 2024 Mattia Giambirtone & All Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## Implementation of Zobrist hashing + +import std/random + + +import pieces + + +type + ZobristKey* = distinct uint64 + ## A zobrist key + + +func `xor`*(a, b: ZobristKey): ZobristKey = ZobristKey(a.uint64 xor b.uint64) + + +proc computeZobristKeys: array[781, ZobristKey] = + ## Precomputes our zobrist keys + var prng = initRand(69420) # Nice. + + # One for each piece on each square + for i in 0..767: + result[i] = ZobristKey(prng.next()) + # One to indicate that it is black's turn + # to move + result[768] = ZobristKey(prng.next()) + # Four numbers to indicate castling rights + for i in 769..773: + result[i] = ZobristKey(prng.next()) + # Eight numbers to indicate the file of a valid + # En passant square, if any + for i in 774..781: + result[i] = ZobristKey(prng.next()) + + + +let ZOBRIST_KEYS = computeZobristKeys() +const PIECE_TO_INDEX = [[0, 1, 2, 3, 4, 5], [6, 7, 8, 9, 10, 11]] + + +proc getKey*(piece: Piece, square: Square): ZobristKey = + let index = PIECE_TO_INDEX[piece.color.int][piece.kind.int] + square.int + return ZOBRIST_KEYS[index] + + +proc getBlackToMoveKey*: ZobristKey = ZOBRIST_KEYS[768] + + +proc getQueenSideCastlingKey*(color: PieceColor): ZobristKey = + case color: + of White: + return ZOBRIST_KEYS[769] + of Black: + return ZOBRIST_KEYS[771] + else: + discard + + +proc getKingSideCastlingKey*(color: PieceColor): ZobristKey = + case color: + of White: + return ZOBRIST_KEYS[770] + of Black: + return ZOBRIST_KEYS[772] + else: + discard + + +proc getEnPassantKey*(file: SomeInteger): ZobristKey = ZOBRIST_KEYS[774 + file] \ No newline at end of file