Remove unnecessary modules and refactor some things. WIP on testing broken Zobrist hashing

This commit is contained in:
Mattia Giambirtone 2024-04-27 15:00:03 +02:00
parent 8eeede55c2
commit 4efde83934
10 changed files with 199 additions and 174 deletions

View File

@ -10,4 +10,4 @@ Just run `nimble install`
# Testing
Just run `nimble test`: sit back, relax, get yourself a cup of coffee and wait for it to finish :)
Just run `nimble test`: sit back, relax, get yourself a cup of coffee and wait for it to finish :)

View File

@ -3,3 +3,4 @@
-d:danger
--passL:"-flto"
--passC:"-Ofast -flto -march=native -mtune=native"
--maxLoopIterationsVM:100000000

View File

@ -13,7 +13,6 @@
# limitations under the License.
import nimfishpkg/tui
import nimfishpkg/misc
import nimfishpkg/movegen
import nimfishpkg/bitboards
import nimfishpkg/moves
@ -24,12 +23,9 @@ import nimfishpkg/position
import nimfishpkg/board
export tui, misc, movegen, bitboards, moves, pieces, magics, rays, position, board
export tui, movegen, bitboards, moves, pieces, magics, rays, position, board
when isMainModule:
basicTests()
setControlCHook(proc () {.noconv.} = quit(0))
quit(commandLoop())

View File

@ -27,7 +27,7 @@ import zobrist
export pieces, position, bitboards, moves, magics, rays
export pieces, position, bitboards, moves, magics, rays, zobrist
@ -255,17 +255,15 @@ func countPieces*(self: Chessboard, piece: Piece): int {.inline.} =
return self.countPieces(piece.kind, piece.color)
func getOccupancyFor*(self: Chessboard, color: PieceColor): Bitboard =
func getOccupancyFor*(self: Chessboard, color: PieceColor): Bitboard {.inline.} =
## Get the occupancy bitboard for every piece of the given color
result = Bitboard(0)
for b in self.position.pieces[color][]:
result = result or b
result = self.position.getOccupancyFor(color)
func getOccupancy*(self: Chessboard): Bitboard {.inline.} =
## Get the occupancy bitboard for every piece on
## the chessboard
result = self.getOccupancyFor(Black) or self.getOccupancyFor(White)
result = self.position.getOccupancy()
func getPawnAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} =
@ -591,15 +589,15 @@ proc toFEN*(self: Chessboard): string =
proc drawByRepetition*(self: Chessboard): bool =
## Returns whether the current position is a draw
## by repetition
# Naive version. TODO: Improve
# TODO: Improve this
var i = self.positions.high()
var count = 0
while i > 0:
if count == 2:
self.position.repetitionDraw = true
return true
if self.position.zobristKey == self.positions[i].zobristKey:
inc(count)
if count == 2:
self.position.repetitionDraw = true
return true
dec(i)
@ -620,7 +618,7 @@ proc hash*(self: Chessboard) =
self.position.zobristKey = self.position.zobristKey xor getQueenSideCastlingKey(White)
if self.position.castlingAvailability[Black.int].king:
self.position.zobristKey = self.position.zobristKey xor getKingSideCastlingKey(Black)
if self.position.castlingAvailability[Black.int].king:
if self.position.castlingAvailability[Black.int].queen:
self.position.zobristKey = self.position.zobristKey xor getQueenSideCastlingKey(Black)
if self.position.enPassantSquare != nullSquare():

View File

@ -1,132 +0,0 @@
# 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.
## Miscellaneous stuff
import board
import std/strformat
import std/strutils
proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead"
proc testPieceCount(board: Chessboard, kind: PieceKind, color: PieceColor, count: int) =
let pieces = board.countPieces(kind, color)
doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead"
proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) =
var i = 0
for square in bitboard:
doAssert squares[i] == square, &"squares[{i}] != bitboard[i]: {squares[i]} != {square}"
inc(i)
if i != squares.len():
doAssert false, &"bitboard.len() ({i}) != squares.len() ({squares.len()})"
const fens = staticRead("../../tests/all.txt").splitLines()
proc basicTests* =
for fen in fens:
doAssert fen == newChessboardFromFEN(fen).toFEN()
var b = newDefaultChessboard()
# Ensure correct number of pieces
testPieceCount(b, Pawn, White, 8)
testPieceCount(b, Pawn, Black, 8)
testPieceCount(b, Knight, White, 2)
testPieceCount(b, Knight, Black, 2)
testPieceCount(b, Bishop, White, 2)
testPieceCount(b, Bishop, Black, 2)
testPieceCount(b, Rook, White, 2)
testPieceCount(b, Rook, Black, 2)
testPieceCount(b, Queen, White, 1)
testPieceCount(b, Queen, Black, 1)
testPieceCount(b, King, White, 1)
testPieceCount(b, King, Black, 1)
# Ensure pieces are in the correct squares. This is testing the FEN
# parser
# Pawns
for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]:
testPiece(b.getPiece(loc), Pawn, White)
for loc in ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]:
testPiece(b.getPiece(loc), Pawn, Black)
# Rooks
testPiece(b.getPiece("a1"), Rook, White)
testPiece(b.getPiece("h1"), Rook, White)
testPiece(b.getPiece("a8"), Rook, Black)
testPiece(b.getPiece("h8"), Rook, Black)
# Knights
testPiece(b.getPiece("b1"), Knight, White)
testPiece(b.getPiece("g1"), Knight, White)
testPiece(b.getPiece("b8"), Knight, Black)
testPiece(b.getPiece("g8"), Knight, Black)
# Bishops
testPiece(b.getPiece("c1"), Bishop, White)
testPiece(b.getPiece("f1"), Bishop, White)
testPiece(b.getPiece("c8"), Bishop, Black)
testPiece(b.getPiece("f8"), Bishop, Black)
# Kings
testPiece(b.getPiece("e1"), King, White)
testPiece(b.getPiece("e8"), King, Black)
# Queens
testPiece(b.getPiece("d1"), Queen, White)
testPiece(b.getPiece("d8"), Queen, Black)
# Ensure our bitboards match with the board
let
whitePawns = b.getBitboard(Pawn, White)
whiteKnights = b.getBitboard(Knight, White)
whiteBishops = b.getBitboard(Bishop, White)
whiteRooks = b.getBitboard(Rook, White)
whiteQueens = b.getBitboard(Queen, White)
whiteKing = b.getBitboard(King, White)
blackPawns = b.getBitboard(Pawn, Black)
blackKnights = b.getBitboard(Knight, Black)
blackBishops = b.getBitboard(Bishop, Black)
blackRooks = b.getBitboard(Rook, Black)
blackQueens = b.getBitboard(Queen, Black)
blackKing = b.getBitboard(King, Black)
whitePawnSquares = @[makeSquare(6'i8, 0'i8), makeSquare(6, 1), makeSquare(6, 2), makeSquare(6, 3), makeSquare(6, 4), makeSquare(6, 5), makeSquare(6, 6), makeSquare(6, 7)]
whiteKnightSquares = @[makeSquare(7'i8, 1'i8), makeSquare(7, 6)]
whiteBishopSquares = @[makeSquare(7'i8, 2'i8), makeSquare(7, 5)]
whiteRookSquares = @[makeSquare(7'i8, 0'i8), makeSquare(7, 7)]
whiteQueenSquares = @[makeSquare(7'i8, 3'i8)]
whiteKingSquares = @[makeSquare(7'i8, 4'i8)]
blackPawnSquares = @[makeSquare(1'i8, 0'i8), makeSquare(1, 1), makeSquare(1, 2), makeSquare(1, 3), makeSquare(1, 4), makeSquare(1, 5), makeSquare(1, 6), makeSquare(1, 7)]
blackKnightSquares = @[makeSquare(0'i8, 1'i8), makeSquare(0, 6)]
blackBishopSquares = @[makeSquare(0'i8, 2'i8), makeSquare(0, 5)]
blackRookSquares = @[makeSquare(0'i8, 0'i8), makeSquare(0, 7)]
blackQueenSquares = @[makeSquare(0'i8, 3'i8)]
blackKingSquares = @[makeSquare(0'i8, 4'i8)]
testPieceBitboard(whitePawns, whitePawnSquares)
testPieceBitboard(whiteKnights, whiteKnightSquares)
testPieceBitboard(whiteBishops, whiteBishopSquares)
testPieceBitboard(whiteRooks, whiteRookSquares)
testPieceBitboard(whiteQueens, whiteQueenSquares)
testPieceBitboard(whiteKing, whiteKingSquares)
testPieceBitboard(blackPawns, blackPawnSquares)
testPieceBitboard(blackKnights, blackKnightSquares)
testPieceBitboard(blackBishops, blackBishopSquares)
testPieceBitboard(blackRooks, blackRookSquares)
testPieceBitboard(blackQueens, blackQueenSquares)
testPieceBitboard(blackKing, blackKingSquares)

View File

@ -14,8 +14,9 @@
## Move generation logic
when not defined(danger):
import std/strformat
import std/strformat
import std/tables
import std/strutils
import bitboards
@ -25,10 +26,9 @@ import pieces
import moves
import position
import rays
import misc
export bitboards, magics, pieces, moves, position, rays, misc, board
export bitboards, magics, pieces, moves, position, rays, board
@ -455,3 +455,142 @@ proc unmakeMove*(self: Chessboard) =
self.position = self.positions.pop()
self.update()
self.hash()
## Testing stuff
proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead"
proc testPieceCount(board: Chessboard, kind: PieceKind, color: PieceColor, count: int) =
let pieces = board.countPieces(kind, color)
doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead"
proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) =
var i = 0
for square in bitboard:
doAssert squares[i] == square, &"squares[{i}] != bitboard[i]: {squares[i]} != {square}"
inc(i)
if i != squares.len():
doAssert false, &"bitboard.len() ({i}) != squares.len() ({squares.len()})"
const testFens = staticRead("../../tests/all.txt").splitLines()
const benchFens = staticRead("../../tests/all.txt").splitLines()
proc basicTests* =
for fen in testFens:
doAssert fen == newChessboardFromFEN(fen).toFEN()
for fen in benchFens:
var
board = newChessboardFromFEN(fen)
hashes = newTable[ZobristKey, Move]()
moves = newMoveList()
board.generateMoves(moves)
for move in moves:
board.makeMove(move)
let
currentFEN = board.toFEN()
pos = board.position
key = pos.zobristKey
board.unmakeMove()
doAssert not hashes.contains(key), &"{fen} has zobrist collisions {move} -> {hashes[key]}"
hashes[key] = move
var board = newDefaultChessboard()
# Ensure correct number of pieces
testPieceCount(board, Pawn, White, 8)
testPieceCount(board, Pawn, Black, 8)
testPieceCount(board, Knight, White, 2)
testPieceCount(board, Knight, Black, 2)
testPieceCount(board, Bishop, White, 2)
testPieceCount(board, Bishop, Black, 2)
testPieceCount(board, Rook, White, 2)
testPieceCount(board, Rook, Black, 2)
testPieceCount(board, Queen, White, 1)
testPieceCount(board, Queen, Black, 1)
testPieceCount(board, King, White, 1)
testPieceCount(board, King, Black, 1)
# Ensure pieces are in the correct squares. This is testing the FEN
# parser
# Pawns
for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]:
testPiece(board.getPiece(loc), Pawn, White)
for loc in ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]:
testPiece(board.getPiece(loc), Pawn, Black)
# Rooks
testPiece(board.getPiece("a1"), Rook, White)
testPiece(board.getPiece("h1"), Rook, White)
testPiece(board.getPiece("a8"), Rook, Black)
testPiece(board.getPiece("h8"), Rook, Black)
# Knights
testPiece(board.getPiece("b1"), Knight, White)
testPiece(board.getPiece("g1"), Knight, White)
testPiece(board.getPiece("b8"), Knight, Black)
testPiece(board.getPiece("g8"), Knight, Black)
# Bishops
testPiece(board.getPiece("c1"), Bishop, White)
testPiece(board.getPiece("f1"), Bishop, White)
testPiece(board.getPiece("c8"), Bishop, Black)
testPiece(board.getPiece("f8"), Bishop, Black)
# Kings
testPiece(board.getPiece("e1"), King, White)
testPiece(board.getPiece("e8"), King, Black)
# Queens
testPiece(board.getPiece("d1"), Queen, White)
testPiece(board.getPiece("d8"), Queen, Black)
# Ensure our bitboards match with the board
let
whitePawns = board.getBitboard(Pawn, White)
whiteKnights = board.getBitboard(Knight, White)
whiteBishops = board.getBitboard(Bishop, White)
whiteRooks = board.getBitboard(Rook, White)
whiteQueens = board.getBitboard(Queen, White)
whiteKing = board.getBitboard(King, White)
blackPawns = board.getBitboard(Pawn, Black)
blackKnights = board.getBitboard(Knight, Black)
blackBishops = board.getBitboard(Bishop, Black)
blackRooks = board.getBitboard(Rook, Black)
blackQueens = board.getBitboard(Queen, Black)
blackKing = board.getBitboard(King, Black)
whitePawnSquares = @[makeSquare(6'i8, 0'i8), makeSquare(6, 1), makeSquare(6, 2), makeSquare(6, 3), makeSquare(6, 4), makeSquare(6, 5), makeSquare(6, 6), makeSquare(6, 7)]
whiteKnightSquares = @[makeSquare(7'i8, 1'i8), makeSquare(7, 6)]
whiteBishopSquares = @[makeSquare(7'i8, 2'i8), makeSquare(7, 5)]
whiteRookSquares = @[makeSquare(7'i8, 0'i8), makeSquare(7, 7)]
whiteQueenSquares = @[makeSquare(7'i8, 3'i8)]
whiteKingSquares = @[makeSquare(7'i8, 4'i8)]
blackPawnSquares = @[makeSquare(1'i8, 0'i8), makeSquare(1, 1), makeSquare(1, 2), makeSquare(1, 3), makeSquare(1, 4), makeSquare(1, 5), makeSquare(1, 6), makeSquare(1, 7)]
blackKnightSquares = @[makeSquare(0'i8, 1'i8), makeSquare(0, 6)]
blackBishopSquares = @[makeSquare(0'i8, 2'i8), makeSquare(0, 5)]
blackRookSquares = @[makeSquare(0'i8, 0'i8), makeSquare(0, 7)]
blackQueenSquares = @[makeSquare(0'i8, 3'i8)]
blackKingSquares = @[makeSquare(0'i8, 4'i8)]
testPieceBitboard(whitePawns, whitePawnSquares)
testPieceBitboard(whiteKnights, whiteKnightSquares)
testPieceBitboard(whiteBishops, whiteBishopSquares)
testPieceBitboard(whiteRooks, whiteRookSquares)
testPieceBitboard(whiteQueens, whiteQueenSquares)
testPieceBitboard(whiteKing, whiteKingSquares)
testPieceBitboard(blackPawns, blackPawnSquares)
testPieceBitboard(blackKnights, blackKnightSquares)
testPieceBitboard(blackBishops, blackBishopSquares)
testPieceBitboard(blackRooks, blackRookSquares)
testPieceBitboard(blackQueens, blackQueenSquares)
testPieceBitboard(blackKing, blackKingSquares)
when isMainModule:
basicTests()

View File

@ -84,6 +84,8 @@ proc toSquare*(s: string): Square {.discardable.} =
proc toAlgebraic*(square: Square): string {.inline.} =
## Converts a square from our internal rank/file
## notation to a square in algebraic notation
if square == nullSquare():
return "null"
let
file = char('a'.uint8 + (square.uint64 and 7))
rank = char('1'.uint8 + ((square.uint64 div 8) xor 7))

View File

@ -19,7 +19,7 @@ import zobrist
type
Position* = object
Position* = ref object
## A chess position
# Castling availability. This just keeps track
@ -80,4 +80,17 @@ func getBitboard*(self: Position, kind: PieceKind, color: PieceColor): Bitboard
func getBitboard*(self: Position, piece: Piece): Bitboard =
## Returns the positional bitboard for the given piece type
return self.getBitboard(piece.kind, piece.color)
return self.getBitboard(piece.kind, piece.color)
func getOccupancyFor*(self: Position, color: PieceColor): Bitboard =
## Get the occupancy bitboard for every piece of the given color
result = Bitboard(0)
for b in self.pieces[color][]:
result = result or b
func getOccupancy*(self: Position): Bitboard {.inline.} =
## Get the occupancy bitboard for every piece on
## the chessboard
result = self.getOccupancyFor(Black) or self.getOccupancyFor(White)

View File

@ -176,9 +176,14 @@ proc qsearch(self: SearchManager, ply: uint8, alpha, beta: Score): Score =
for move in moves:
self.board.doMove(move)
inc(self.nodeCount)
# Find the best move for us (worst move
# for our opponent, hence the negative sign)
var score = -self.qsearch(ply + 1, -beta, -alpha)
var score: Score
if self.board.position.halfMoveClock >= 100 or self.board.position.repetitionDraw:
# Drawing by repetition is *bad*
score = Score(0)
else:
# Find the best move for us (worst move
# for our opponent, hence the negative sign)
score = -self.qsearch(ply + 1, -beta, -alpha)
self.board.unmakeMove()
bestScore = max(score, bestScore)
if score >= beta:
@ -198,17 +203,18 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.d
## Simple negamax search with alpha-beta pruning
if self.shouldStop():
return
# let query = self.transpositionTable.get(self.board.position.zobristKey, depth.uint8)
# if query.success:
# case query.entry.flag:
# of Exact:
# return query.entry.score
# of LowerBound:
# if query.entry.score >= beta:
# return query.entry.score
# of UpperBound:
# if query.entry.score <= alpha:
# return query.entry.score
when defined(useTT):
let query = self.transpositionTable.get(self.board.position.zobristKey, depth.uint8)
if query.success:
case query.entry.flag:
of Exact:
return query.entry.score
of LowerBound:
if query.entry.score >= beta:
return query.entry.score
of UpperBound:
if query.entry.score <= alpha:
return query.entry.score
if depth == 0:
# Quiescent search gain: 264.8 +/- 71.6
return self.qsearch(0, alpha, beta)
@ -246,11 +252,11 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.d
const reduction = 1
score = -self.search(depth - 1 - reduction, ply + 1, -beta, -alpha)
fullDepth = score > alpha]#
#if fullDepth:
score = -self.search(depth - 1 #[+ extension]#, ply + 1, -beta, -alpha)
if self.board.position.halfMoveClock >= 100 or self.board.position.repetitionDraw:
# Drawing by repetition is *bad*
score = Score(0)
#if fullDepth:
score = -self.search(depth - 1 #[+ extension]#, ply + 1, -beta, -alpha)
self.board.unmakeMove()
# When a search is cancelled or times out, we need
# to make sure the entire call stack unwinds back
@ -259,7 +265,8 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.d
return
bestScore = max(score, bestScore)
let nodeType = if score >= beta: LowerBound elif score <= alpha: UpperBound else: Exact
# self.transpositionTable.store(depth.uint8, score, self.board.position.zobristKey, nodeType)
when defined(useTT):
self.transpositionTable.store(depth.uint8, score, self.board.position.zobristKey, nodeType)
if nodeType == LowerBound:
# score >= beta
# This move was too good for us, opponent will not search it

View File

@ -271,8 +271,8 @@ proc parseUCICommand(session: UCISession, command: string): UCICommand =
return session.handleUCIGoCommand(cmd)
of "setoption":
result = UCICommand(kind: SetOption)
inc(current)
while current < cmd.len():
inc(current)
case cmd[current]:
of "name":
inc(current)
@ -282,6 +282,7 @@ proc parseUCICommand(session: UCISession, command: string): UCICommand =
result.value = cmd[current]
else:
discard
inc(current)
else:
# Unknown UCI commands should be ignored. Attempt
@ -333,7 +334,7 @@ proc startUCISession* =
var
cmd: UCICommand
cmdStr: string
session = UCISession(hashTableSize: 64)
session = UCISession(hashTableSize: 64, board: newDefaultChessboard())
while true:
try:
cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '})