diff --git a/Chess/README.md b/Chess/README.md index 3e1ef57..81c4389 100644 --- a/Chess/README.md +++ b/Chess/README.md @@ -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 :) \ No newline at end of file +Just run `nimble test`: sit back, relax, get yourself a cup of coffee and wait for it to finish :) diff --git a/Chess/nim.cfg b/Chess/nim.cfg index b95609e..2ce2caa 100644 --- a/Chess/nim.cfg +++ b/Chess/nim.cfg @@ -3,3 +3,4 @@ -d:danger --passL:"-flto" --passC:"-Ofast -flto -march=native -mtune=native" +--maxLoopIterationsVM:100000000 \ No newline at end of file diff --git a/Chess/nimfish/nimfish.nim b/Chess/nimfish/nimfish.nim index ddbaeb4..21fb537 100644 --- a/Chess/nimfish/nimfish.nim +++ b/Chess/nimfish/nimfish.nim @@ -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()) \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/board.nim b/Chess/nimfish/nimfishpkg/board.nim index 5c7d4e9..fbdc16e 100644 --- a/Chess/nimfish/nimfishpkg/board.nim +++ b/Chess/nimfish/nimfishpkg/board.nim @@ -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(): diff --git a/Chess/nimfish/nimfishpkg/misc.nim b/Chess/nimfish/nimfishpkg/misc.nim deleted file mode 100644 index 8f044b2..0000000 --- a/Chess/nimfish/nimfishpkg/misc.nim +++ /dev/null @@ -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) \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/movegen.nim b/Chess/nimfish/nimfishpkg/movegen.nim index 092ec4a..80821c2 100644 --- a/Chess/nimfish/nimfishpkg/movegen.nim +++ b/Chess/nimfish/nimfishpkg/movegen.nim @@ -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() \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/pieces.nim b/Chess/nimfish/nimfishpkg/pieces.nim index 068055b..f170995 100644 --- a/Chess/nimfish/nimfishpkg/pieces.nim +++ b/Chess/nimfish/nimfishpkg/pieces.nim @@ -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)) diff --git a/Chess/nimfish/nimfishpkg/position.nim b/Chess/nimfish/nimfishpkg/position.nim index 946a927..30c18cf 100644 --- a/Chess/nimfish/nimfishpkg/position.nim +++ b/Chess/nimfish/nimfishpkg/position.nim @@ -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) \ No newline at end of file + 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) diff --git a/Chess/nimfish/nimfishpkg/search.nim b/Chess/nimfish/nimfishpkg/search.nim index a85ce42..323d0bd 100644 --- a/Chess/nimfish/nimfishpkg/search.nim +++ b/Chess/nimfish/nimfishpkg/search.nim @@ -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 diff --git a/Chess/nimfish/nimfishpkg/uci.nim b/Chess/nimfish/nimfishpkg/uci.nim index 52f0451..f678ede 100644 --- a/Chess/nimfish/nimfishpkg/uci.nim +++ b/Chess/nimfish/nimfishpkg/uci.nim @@ -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', ' '})