Compare commits

...

2 Commits

12 changed files with 214 additions and 181 deletions

4
.gitignore vendored
View File

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

12
Chess/.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
# ---> Nim
nimcache/
nimblecache/
htmldocs/
bin
nimfish/nimfishpkg/resources/*.epd
nimfish/nimfishpkg/resources/*.pgn
# Python
__pycache__
fast-chess
log.txt
config.json

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,18 +203,20 @@ 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:
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)
var moves = newMoveList()
var depth = depth
@ -231,13 +238,12 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.d
if ply == 0 and self.searchMoves.len() > 0 and move notin self.searchMoves:
continue
self.board.doMove(move)
var extension = self.getSearchExtension(move)
let zobrist = self.board.position.zobristKey
# var extension = self.getSearchExtension(move)
inc(self.nodeCount)
# Find the best move for us (worst move
# for our opponent, hence the negative sign)
var score: Score
var fullDepth = true
#[var fullDepth = true
if extension == 0 and i >= 3 and not move.isCapture():
# Late Move Reduction: assume our move orderer did a good job,
# so it is not worth to look at all moves at the same depth equally.
@ -245,12 +251,12 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.d
# it at full depth
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)
fullDepth = score > 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, zobrist, 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', ' '})