Initial work on Zobrist hashing. Add Pohl opening book

This commit is contained in:
Mattia Giambirtone 2024-04-25 18:53:51 +02:00
parent 720645092e
commit b74bb3b4ba
9 changed files with 150 additions and 18 deletions

2
.gitignore vendored
View File

@ -3,5 +3,7 @@ nimcache/
nimblecache/
htmldocs/
bin
Chess/nimfish/nimfishpkg/resources/Pohl.epd
Chess/nimfish/nimfishpkg/resources/*.pgn
# Python
__pycache__

View File

@ -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))

View File

@ -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

View File

@ -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()

Binary file not shown.

View File

@ -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

View File

@ -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:

View File

@ -37,6 +37,7 @@ type
Quit,
Debug,
Position,
SetOption,
Go,
Stop

View File

@ -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]