Initial release

Former-commit-id: 5636618959
This commit is contained in:
Mattia Giambirtone 2024-06-12 11:12:21 +02:00
parent 29ee1aafd7
commit 4c6a38e662
Signed by: nocturn9x
GPG Key ID: B6025DD9B4458B69
31 changed files with 7247 additions and 1 deletions

View File

@ -1,3 +1,89 @@
# heimdall
A UCI chess engine written in Nim
A UCI chess engine written in Nim
## Installation
Just run `nimble install` (Nim 2.0.4 or greater is required, see [here](https://github.com/dom96/choosenim))
## Testing
Just run `nimble test`: sit back, relax, get yourself a cup of coffee and wait for it to finish :)
## Features
List of features that are either already implemented or planned
### Search
- [X] Null move pruning
- [X] Late move reductions (log formula)
- [X] Quiescent search
- [X] Aspiration windows
- [X] Futility pruning
- [X] Move reordering
- [X] Alpha-beta pruning
- [X] Check extensions
- [X] QSEE pruning
- [X] Reverse futility pruning
- [X] Principal variation search
- [X] Iterative deepening
- [X] Transposition table
- [X] Cutoffs
- [X] Move ordering
- [X] MVV-LVA
- [X] Static exchange evaluation
- [X] History heuristic
- [X] History gravity
- [ ] History malus
- [X] History aging
- [X] Killer heuristic
- [X] Null-window search
- [X] Parallel search (lazy SMP)
- [X] Pondering
- [ ] Capture history
- [ ] Continuation history
- [ ] Late move pruning
- [ ] Counter moves
- [ ] Razoring
- [ ] Internal iterative reductions
- [ ] Internal iterative deepening
### Eval
- [X] Piece-square tables
- [X] Material
- [X] Tempo
- [ ] King safety
- [X] Virtual queen mobility
- [ ] Pawn shield
- [ ] Pawn storm
- [X] King zone attacks
- [X] Mobility (sliders and knights)
- [X] Mask off pawn attacks
- [ ] Consider pins
- [ ] Minor piece outpost
- [X] Bishop pair
- [X] Rook on (semi-)open file
- [ ] Queen on (semi-)open file
- [ ] Connected rooks
- [ ] Threats
- [ ] Pieces attacked by pawns
- [ ] Major pieces attacked by minor pieces
- [ ] Queens attacked by rooks
- [X] Pawn structure
- [X] Isolated pawns
- [X] Strong (aka protected) pawns
- [ ] Doubled pawns
- [X] Passed pawns
- [ ] Phalanx pawns
## More info
Heimdall is available on [Lichess](https://lichess.org/@/Nimfish) under its old name (Nimfish), feel free to challenge it!
I try to keep the engine running on there always up to date with the changes on the master branch

23
heimdall.nimble Normal file
View File

@ -0,0 +1,23 @@
# Package
version = "0.1.0"
author = "nocturn9x"
description = "A UCI chess engine written in nim"
license = "Apache-2.0"
srcDir = "heimdall"
binDir = "bin"
installExt = @["nim"]
bin = @["heimdall"]
# Dependencies
requires "nim >= 2.0.4"
requires "jsony >= 1.1.5"
requires "nint128 >= 0.3.3"
requires "nimpy >= 0.2.0"
requires "scinim >= 0.2.5"
task test, "Runs the test suite":
exec "python tests/suite.py -d 6 -b -p -s"
exec "python tests/suite.py -d 7 -b -p -s -f tests/heavy.txt"

32
heimdall/heimdall.nim Normal file
View File

@ -0,0 +1,32 @@
# 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.
import heimdallpkg/tui
import heimdallpkg/movegen
import heimdallpkg/bitboards
import heimdallpkg/moves
import heimdallpkg/pieces
import heimdallpkg/magics
import heimdallpkg/rays
import heimdallpkg/position
import heimdallpkg/board
export tui, movegen, bitboards, moves, pieces, magics, rays, position, board
when isMainModule:
setControlCHook(proc () {.noconv.} = quit(0))
basicTests()
quit(commandLoop())

View File

@ -0,0 +1,379 @@
# 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.
## Implements low-level bit operations
import std/bitops
import std/strutils
import pieces
import moves
type
Bitboard* = distinct uint64
## A bitboard
Direction* = enum
## A move direction enumeration
Forward,
Backward,
Left,
Right
ForwardLeft,
ForwardRight,
BackwardLeft,
BackwardRight
# Overloaded operators and functions for our bitboard type
func `shl`*(a: Bitboard, x: Natural): Bitboard {.borrow.}
func `shr`*(a: Bitboard, x: Natural): Bitboard {.borrow.}
func `and`*(a, b: Bitboard): Bitboard {.borrow.}
func `or`*(a, b: Bitboard): Bitboard {.borrow.}
func `not`*(a: Bitboard): Bitboard {.borrow.}
func `shr`*(a, b: Bitboard): Bitboard {.borrow.}
func `xor`*(a, b: Bitboard): Bitboard {.borrow.}
func `+`*(a, b: Bitboard): Bitboard {.borrow.}
func `-`*(a, b: Bitboard): Bitboard {.borrow.}
func `div`*(a, b: Bitboard): Bitboard {.borrow.}
func `*`*(a, b: Bitboard): Bitboard {.borrow.}
func `+`*(a: Bitboard, b: SomeUnsignedInt): Bitboard {.borrow.}
func `-`*(a: Bitboard, b: SomeUnsignedInt): Bitboard {.borrow.}
func `div`*(a: Bitboard, b: SomeUnsignedInt): Bitboard {.borrow.}
func `*`*(a: Bitboard, b: SomeUnsignedInt): Bitboard {.borrow.}
func `*`*(a: SomeUnsignedInt, b: Bitboard): Bitboard {.borrow.}
func `==`*(a, b: Bitboard): bool {.inline, borrow.}
func `==`*(a: Bitboard, b: SomeInteger): bool {.inline.} = a.uint64 == b.uint64
func `!=`*(a, b: Bitboard): bool {.inline.} = a.uint64 != b.uint64
func `!=`*(a: Bitboard, b: SomeInteger): bool {.inline.} = a.uint64 != b.uint64
func countSetBits*(a: Bitboard): int = a.uint64.countSetBits()
func countLeadingZeroBits*(a: Bitboard): int {.borrow.}
func countTrailingZeroBits*(a: Bitboard): int {.borrow.}
func clearBit*(a: var Bitboard, bit: SomeInteger) {.borrow.}
func setBit*(a: var Bitboard, bit: SomeInteger) {.borrow.}
func clearBit*(a: var Bitboard, bit: Square) {.borrow.}
func setBit*(a: var Bitboard, bit: Square) {.borrow.}
func removed*(a, b: Bitboard): Bitboard = a and not b
func countSquares*(self: Bitboard): int {.inline.} =
## Returns the number of active squares
## in the bitboard
result = self.countSetBits()
func lowestSquare*(self: Bitboard): Square {.inline.} =
## Returns the index of the lowest set bit
## in the given bitboard as a square
result = Square(self.countTrailingZeroBits().uint8)
func highestSquare*(self: Bitboard): Square {.inline.} =
## Returns the index of the highest set bit
## in the given bitboard as a square
result = Square(self.countLeadingZeroBits().uint8 xor 0x3f)
func getFileMask*(file: int): Bitboard {.inline.} = Bitboard(0x101010101010101'u64) shl file.uint64
func getRankMask*(rank: int): Bitboard {.inline.} = Bitboard(0xff) shl uint64(8 * rank)
func toBitboard*(square: SomeInteger): Bitboard {.inline.} = Bitboard(1'u64) shl square.uint64
func toBitboard*(square: Square): Bitboard {.inline.} = toBitboard(square.int8)
func toSquare*(b: Bitboard): Square {.inline.} = Square(b.uint64.countTrailingZeroBits())
func createMove*(startSquare: Bitboard, targetSquare: Square, flags: varargs[MoveFlag]): Move {.inline.} =
result = createMove(startSquare.toSquare(), targetSquare, flags)
func createMove*(startSquare: Square, targetSquare: Bitboard, flags: varargs[MoveFlag]): Move {.inline.} =
result = createMove(startSquare, targetSquare.toSquare(), flags)
func createMove*(startSquare, targetSquare: Bitboard, flags: varargs[MoveFlag]): Move {.inline.} =
result = createMove(startSquare.toSquare(), targetSquare.toSquare(), flags)
func toBin*(x: Bitboard, b: Positive = 64): string {.inline.} = toBin(BiggestInt(x), b)
func toBin*(x: uint64, b: Positive = 64): string {.inline.} = toBin(Bitboard(x), b)
func contains*(self: Bitboard, square: Square): bool {.inline.} = (self and square.toBitboard()) != 0
iterator items*(self: Bitboard): Square =
## Iterates ove the given bitboard
## and returns all the squares that
## are set
var bits = self
while bits != 0:
yield bits.toSquare()
bits = bits and bits - 1
iterator subsets*(self: Bitboard): Bitboard =
## Iterates over all the subsets of the given
## bitboard using the Carry-Rippler trick
# Thanks analog-hors :D
var subset = Bitboard(0)
while true:
subset = (subset - self) and self
yield subset
if subset == 0:
break
iterator pairs*(self: Bitboard): tuple[i: int, sq: Square] =
var i = 0
for item in self:
yield (i, item)
inc(i)
func pretty*(self: Bitboard): string =
iterator items(self: Bitboard): uint8 =
## Iterates over all the bits in the
## given bitboard
for i in 0..63:
yield self.uint64.bitsliced(i..i).uint8
iterator pairs(self: Bitboard): (int, uint8) =
var i = 0
for bit in self:
yield (i, bit)
inc(i)
## Returns a prettyfied version of
## the given bitboard
result &= "- - - - - - - -\n"
for i, bit in self:
if i > 0 and i mod 8 == 0:
result &= "\n"
result &= $bit & " "
result &= "\n- - - - - - - -"
func `$`*(self: Bitboard): string {.inline.} = self.pretty()
func getDirectionMask*(bitboard: Bitboard, color: PieceColor, direction: Direction): Bitboard =
## Get a bitmask relative to the given bitboard
## for the given direction for a piece of the
## given color
case color:
of White:
case direction:
of Forward:
return bitboard shr 8
of Backward:
return bitboard shl 8
of ForwardRight:
return bitboard shr 7
of ForwardLeft:
return bitboard shr 9
of BackwardRight:
return bitboard shl 9
of BackwardLeft:
return bitboard shl 7
of Left:
return bitboard shr 1
of Right:
return bitboard shl 1
of Black:
case direction:
of Forward:
return bitboard shl 8
of Backward:
return bitboard shr 8
of ForwardRight:
return bitboard shl 7
of ForwardLeft:
return bitboard shl 9
of BackwardRight:
return bitboard shr 9
of BackwardLeft:
return bitboard shr 7
of Left:
return bitboard shl 1
of Right:
return bitboard shr 1
else:
discard
func getEighthRank*(color: PieceColor): Bitboard {.inline.} = (if color == White: getRankMask(0) else: getRankMask(7))
func getFirstRank*(color: PieceColor): Bitboard {.inline.} = (if color == White: getRankMask(7) else: getRankMask(0))
func getSeventhRank*(color: PieceColor): Bitboard {.inline.} = (if color == White: getRankMask(1) else: getRankMask(6))
func getSecondRank*(color: PieceColor): Bitboard {.inline.} = (if color == White: getRankMask(6) else: getRankMask(1))
func getLeftmostFile*(color: PieceColor): Bitboard {.inline.}= (if color == White: getFileMask(0) else: getFileMask(7))
func getRightmostFile*(color: PieceColor): Bitboard {.inline.} = (if color == White: getFileMask(7) else: getFileMask(0))
func getDirectionMask*(square: Square, color: PieceColor, direction: Direction): Bitboard {.inline.} =
## Get a bitmask for the given direction for a piece
## of the given color located at the given square
result = getDirectionMask(toBitboard(square), color, direction)
func forwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = getDirectionMask(self, side, Forward)
func doubleForwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.forwardRelativeTo(side).forwardRelativeTo(side)
func backwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = getDirectionMask(self, side, Backward)
func doubleBackwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.backwardRelativeTo(side).backwardRelativeTo(side)
func leftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = getDirectionMask(self, side, Left) and not getRightmostFile(side)
func rightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = getDirectionMask(self, side, Right) and not getLeftmostFile(side)
# We mask off the opposite files to make sure there are
# no weird wraparounds when moving at the edges
func forwardRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} =
getDirectionMask(self, side, ForwardRight) and not getLeftmostFile(side)
func forwardLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} =
getDirectionMask(self, side, ForwardLeft) and not getRightmostFile(side)
func backwardRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} =
getDirectionMask(self, side, BackwardRight) and not getLeftmostFile(side)
func backwardLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} =
getDirectionMask(self, side, BackwardLeft) and not getRightmostFile(side)
func longKnightUpLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleForwardRelativeTo(side).leftRelativeTo(side)
func longKnightUpRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleForwardRelativeTo(side).rightRelativeTo(side)
func longKnightDownLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleBackwardRelativeTo(side).leftRelativeTo(side)
func longKnightDownRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleBackwardRelativeTo(side).rightRelativeTo(side)
func shortKnightUpLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.forwardRelativeTo(side).leftRelativeTo(side).leftRelativeTo(side)
func shortKnightUpRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.forwardRelativeTo(side).rightRelativeTo(side).rightRelativeTo(side)
func shortKnightDownLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.backwardRelativeTo(side).leftRelativeTo(side).leftRelativeTo(side)
func shortKnightDownRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.backwardRelativeTo(side).rightRelativeTo(side).rightRelativeTo(side)
# We precompute as much stuff as possible: lookup tables are fast!
func computeKingBitboards: array[Square(0)..Square(63), Bitboard] {.compileTime.} =
## Precomputes all the movement bitboards for the king
for i in Square(0)..Square(63):
let king = i.toBitboard()
# It doesn't really matter which side we generate
# the move for, they're identical for both
var movements = king.forwardRelativeTo(White)
movements = movements or king.forwardLeftRelativeTo(White)
movements = movements or king.leftRelativeTo(White)
movements = movements or king.rightRelativeTo(White)
movements = movements or king.backwardRelativeTo(White)
movements = movements or king.forwardRightRelativeTo(White)
movements = movements or king.backwardRightRelativeTo(White)
movements = movements or king.backwardLeftRelativeTo(White)
# We don't *need* to mask the king off: the engine already masks off
# the board's occupancy when generating moves, but it may be useful for
# other parts of the movegen for this stuff not to say "the king can just
# stay still", so we do it anyway
movements = movements and not king
result[i] = movements
func computeKnightBitboards: array[Square(0)..Square(63), Bitboard] {.compileTime.} =
## Precomputes all the movement bitboards for knights
for i in Square(0)..Square(63):
let knight = i.toBitboard()
# It doesn't really matter which side we generate
# the move for, they're identical for both
var movements = knight.longKnightDownLeftRelativeTo(White)
movements = movements or knight.longKnightDownRightRelativeTo(White)
movements = movements or knight.longKnightUpLeftRelativeTo(White)
movements = movements or knight.longKnightUpRightRelativeTo(White)
movements = movements or knight.shortKnightDownLeftRelativeTo(White)
movements = movements or knight.shortKnightDownRightRelativeTo(White)
movements = movements or knight.shortKnightUpLeftRelativeTo(White)
movements = movements or knight.shortKnightUpRightRelativeTo(White)
movements = movements and not knight
result[i] = movements
func computePawnAttacks(color: PieceColor): array[Square(0)..Square(63), Bitboard] {.compileTime.} =
## Precomputes all the attack bitboards for pawns
## of the given color
for i in Square(0)..Square(63):
let pawn = i.toBitboard()
result[i] = pawn.backwardLeftRelativeTo(color) or pawn.backwardRightRelativeTo(color)
func computePassedPawnMasks(color: PieceColor): array[Square(0)..Square(63), Bitboard] =
## Precomputes all the masks for passed pawns of the
## given color
for square in Square(0)..Square(63):
let file = fileFromSquare(square)
let rank = rankFromSquare(square)
result[square] = getFileMask(file)
if file + 1 in 0..7:
result[square] = result[square] or (getFileMask(file + 1))
if file - 1 in 0..7:
result[square] = result[square] or (getFileMask(file - 1))
if color == White:
result[square] = result[square] shr (8 * (7 - rank))
else:
result[square] = result[square] shl (8 * (rank))
result[square] = result[square] and not getRankMask(0)
result[square] = result[square] and not getRankMask(7)
func computeIsolatedPawnMasks: array[8, Bitboard] {.compileTime.} =
## Computes all the masks for isolated pawns
for file in 0..7:
if file - 1 in 0..7:
result[file] = result[file] or getFileMask(file - 1)
if file + 1 in 0..7:
result[file] = result[file] or getFileMask(file + 1)
result[file] = result[file] and not getRankMask(0)
result[file] = result[file] and not getRankMask(7)
func computeKingZoneMasks(color: PieceColor): array[64, Bitboard] {.compileTime.} =
## Computes the king zone masks for the given
## color at compile time
for square in Square(0)..Square(63):
let squareBB = square.toBitboard()
# Front side
result[square.int] = squareBB.forwardRelativeTo(color) or squareBB.forwardLeftRelativeTo(color) or squareBB.forwardRightRelativeTo(color)
# Back side
result[square.int] = result[square.int] or (squareBB.backwardRelativeTo(color) or squareBB.backwardLeftRelativeTo(color) or squareBB.backwardRightRelativeTo(color))
# Flanks
result[square.int] = result[square.int] or (squareBB.leftRelativeTo(color) or squareBB.rightRelativeTo(color))
const
KING_BITBOARDS = computeKingBitboards()
KNIGHT_BITBOARDS = computeKnightBitboards()
PAWN_ATTACKS: array[PieceColor.White..PieceColor.Black, array[Square(0)..Square(63), Bitboard]] = [computePawnAttacks(White), computePawnAttacks(Black)]
KING_ZONE_MASKS: array[PieceColor.White..PieceColor.Black, array[Square(0)..Square(63), Bitboard]] = [computeKingZoneMasks(White),
computeKingZoneMasks(Black)]
ISOLATED_PAWNS = computeIsolatedPawnMasks()
let PASSED_PAWNS: array[PieceColor.White..PieceColor.Black, array[Square(0)..Square(63), Bitboard]] = [computePassedPawnMasks(White), computePassedPawnMasks(Black)]
func getKingAttacks*(square: Square): Bitboard {.inline.} = KING_BITBOARDS[square]
func getKnightAttacks*(square: Square): Bitboard {.inline.} = KNIGHT_BITBOARDS[square]
func getPawnAttacks*(color: PieceColor, square: Square): Bitboard {.inline.} = PAWN_ATTACKS[color][square]
proc getPassedPawnMask*(color: PieceColor, square: Square): Bitboard {.inline.} = PASSED_PAWNS[color][square]
proc getKingZoneMask*(color: PieceColor, square: Square): Bitboard {.inline.} = KING_ZONE_MASKS[color][square]
func getIsolatedPawnMask*(file: int): Bitboard {.inline.} = ISOLATED_PAWNS[file]

View File

@ -0,0 +1,169 @@
# 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 a simple chessboard
import pieces
import magics
import moves
import rays
import bitboards
import position
import zobrist
export pieces, position, bitboards, moves, magics, rays, zobrist
type
Chessboard* = ref object
## A chessboard
# List of all reached positions
positions*: seq[Position]
func toFEN*(self: Chessboard): string
proc newChessboardFromFEN*(fen: string): Chessboard =
## Initializes a chessboard with the
## position encoded by the given FEN string
new(result)
result.positions.add(loadFEN(fen))
proc newDefaultChessboard*: Chessboard {.inline.} =
## Initializes a chessboard with the
## starting position
return newChessboardFromFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
func inCheck*(self: Chessboard): bool {.inline.} =
## Returns if the current side to move is in check
return self.positions[^1].inCheck()
proc canCastle*(self: Chessboard): tuple[queen, king: bool] {.inline.} =
## Returns if the current side to move can castle
return self.positions[^1].canCastle()
func `$`*(self: Chessboard): string = $self.positions[^1]
func pretty*(self: Chessboard): string =
## Returns a colored version of the
## current position for easier visualization
return self.positions[^1].pretty()
func toFEN*(self: Chessboard): string =
## Returns a FEN string of the current
## position in the chessboard
return self.positions[^1].toFEN()
func drawnByRepetition*(self: Chessboard): bool =
## Returns whether the current position is a draw
## by repetition
# TODO: Improve this
var i = self.positions.high() - 1
var count = 0
while i >= 0:
if self.positions[^1].zobristKey == self.positions[i].zobristKey:
inc(count)
if count == 2:
return true
if self.positions[i].halfMoveClock == 0:
# Position was reached via a pawn move or
# capture: cannot repeat beyond this point!
return false
dec(i)
proc isInsufficientMaterial*(self: Chessboard): bool =
## Returns whether the current position is drawn
## due to insufficient mating material. Note that
## this is not a strict implementation of the FIDE
## rule about material draws due to the fact that
## it would be basically impossible to implement those
## efficiently
# Break out early if there's more than 4 pieces on the
# board
let occupancy = self.positions[^1].getOccupancy()
if occupancy.countSquares() > 4:
return false
# KvK is a draw
if occupancy.countSquares() == 2:
# Only the two kings are left
return true
let
sideToMove = self.positions[^1].sideToMove
nonSideToMove = sideToMove.opposite()
# Break out early if there's any pawns left on the board
if self.positions[^1].getBitboard(Pawn, sideToMove) != 0:
return false
if self.positions[^1].getBitboard(Pawn, nonSideToMove) != 0:
return false
# If there's any queens or rooks on the board, break out early too
let
friendlyQueens = self.positions[^1].getBitboard(Queen, sideToMove)
enemyQueens = self.positions[^1].getBitboard(Queen, nonSideToMove)
friendlyRooks = self.positions[^1].getBitboard(Rook, sideToMove)
enemyRooks = self.positions[^1].getBitboard(Rook, nonSideToMove)
if (friendlyQueens or enemyQueens or friendlyRooks or enemyRooks).countSquares() != 0:
return false
# KNvK is a draw
let knightCount = (self.positions[^1].getBitboard(Knight, sideToMove) or self.positions[^1].getBitboard(Knight, nonSideToMove)).countSquares()
# More than one knight (doesn't matter which side), not a draw
if knightCount > 1:
return false
# KBvK is a draw
let bishopCount = (self.positions[^1].getBitboard(Bishop, sideToMove) or self.positions[^1].getBitboard(Bishop, nonSideToMove)).countSquares()
if bishopCount + knightCount > 1:
return false
# Maybe TODO: KBBvK and KBvKB (these should be handled by search anyway)
return true
func isDrawn*(self: Chessboard): bool =
## Returns whether the given position is
## drawn
if self.positions[^1].halfMoveClock >= 100:
# Draw by 50 move rule
return true
if self.drawnByRepetition():
return true
if self.isInsufficientMaterial():
return true

View File

@ -0,0 +1,662 @@
# 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.
## Position evaluation utilities
import pieces
import position
import weights
import nimpy
import scinim/numpyarrays
import arraymancer
export Score
type
Features* = ref object of PyNimObjectExperimental
## The features of our evaluation
## represented as a linear system
# Our piece-square tables contain positional bonuses
# (and maluses). We have one for each game phase (middle
# and end game) for each piece
psqts: array[PieceKind.Bishop..PieceKind.Rook, array[Square(0)..Square(63), tuple[mg, eg: float]]]
# These are the relative values of each piece in the middle game and endgame
pieceWeights: array[PieceKind.Bishop..PieceKind.Rook, tuple[mg, eg: float]]
# Bonus for being the side to move
tempo: float
# Bonuses for rooks on open files
rookOpenFile: tuple[mg, eg: float]
# Bonuses for rooks on semi-open files
rookSemiOpenFile: tuple[mg, eg: float]
# PSQTs for passed pawns (2 per phase)
passedPawnBonuses: array[Square(0)..Square(63), tuple[mg, eg: float]]
# PSQTs for isolated pawns (2 per phase)
isolatedPawnBonuses: array[Square(0)..Square(63), tuple[mg, eg: float]]
# Mobility bonuses
bishopMobility: array[14, tuple[mg, eg: float]]
knightMobility: array[9, tuple[mg, eg: float]]
rookMobility: array[15, tuple[mg, eg: float]]
queenMobility: array[28, tuple[mg, eg: float]]
virtualQueenMobility: array[28, tuple[mg, eg: float]]
# King zone attacks
kingZoneAttacks: array[9, tuple[mg, eg: float]]
# Bonuses for when the rooks are connected
connectedRooks: tuple[mg, eg: float]
# Bonuses for having the bishop pair
bishopPair: tuple[mg, eg: float]
# Maluses for doubled pawns
doubledPawns: tuple[mg, eg: float]
# Bonuses for strong pawns
strongPawns: tuple[mg, eg: float]
# Threats
# Pawns attacking minor pieces
pawnMinorThreats: tuple[mg, eg: float]
# Pawns attacking major pieces
pawnMajorThreats: tuple[mg, eg: float]
# Minor pieces attacking major ones
minorMajorThreats: tuple[mg, eg: float]
# Rooks attacking queens
rookQueenThreats: tuple[mg, eg: float]
EvalMode* = enum
## An enumeration of evaluation
## modes
Default, # Run the evaluation as normal
Tune # Run the evaluation in tuning mode:
# this turns the evaluation into a
# 1D feature vector to be used for
# tuning purposes
func lowestEval*: Score {.inline.} = Score(-30_000)
func highestEval*: Score {.inline.} = Score(30_000)
func mateScore*: Score {.inline.} = highestEval()
func getGamePhase(position: Position): int {.inline.} =
## Computes the game phase according to
## how many pieces are left on the board
result = 0
for sq in position.getOccupancy():
case position.getPiece(sq).kind:
of Bishop, Knight:
inc(result)
of Queen:
inc(result, 4)
of Rook:
inc(result, 2)
else:
discard
# Caps the value in case of early
# promotions
result = min(24, result)
proc getPieceScore*(position: Position, square: Square): Score =
## Returns the value of the piece located at
## the given square given the current game phase
let
piece = position.getPiece(square)
middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color][piece.kind][square]
endGameScore = ENDGAME_VALUE_TABLES[piece.color][piece.kind][square]
middleGamePhase = position.getGamePhase()
endGamePhase = 24 - middleGamePhase
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
proc getPieceScore*(position: Position, piece: Piece, square: Square): Score =
## Returns the value the given piece would have if it
## were at the given square given the current game phase
let
middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color][piece.kind][square]
endGameScore = ENDGAME_VALUE_TABLES[piece.color][piece.kind][square]
middleGamePhase = position.getGamePhase()
endGamePhase = 24 - middleGamePhase
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
proc getMobility(position: Position, square: Square): Bitboard =
## Returns the bitboard of moves a piece can make as far as our mobility
## calculation is concerned. This doesn't necessarily return the number
## of legal moves for a piece (for example, queens may be allowed to X-ray
## through friendly bishops or rooks, or even other queens). We also calculate
## a virtual mobility for the king as if it were a queen (for king safety)
let piece = position.getPiece(square)
result = Bitboard(0)
case piece.kind:
of Queen, Rook, Bishop, King:
let occupancy = position.getOccupancy()
if piece.kind in [Rook, Queen, King]:
let blockers = occupancy and Rook.getRelevantBlockers(square)
result = getRookMoves(square, blockers)
if piece.kind in [Bishop, Queen, King]:
let blockers = occupancy and Bishop.getRelevantBlockers(square)
result = result or getBishopMoves(square, blockers)
of Knight:
result = getKnightAttacks(square)
else:
discard
# We don't mask anything off when computing virtual queen
# mobility because it is a representation of the potential
# attack vectors of the opponent rather than a measure of
# how much a piece can/should move
if piece.kind != King and result != 0:
# Mask off friendly pieces
result = result and not position.getOccupancyFor(piece.color)
# Mask off squares attacked by enemy pawns
let
enemyColor = piece.color.opposite()
enemyPawns = position.getBitboard(Pawn, enemyColor)
attackedSquares = enemyPawns.forwardRightRelativeTo(enemyColor) or enemyPawns.forwardLeftRelativeTo(enemyColor)
result = result and not attackedSquares
# TODO: Take pins into account
proc getAttackingMoves(position: Position, square: Square): Bitboard =
## Returns the bitboard of possible attacks from the
## piece on the given square
let piece = position.getPiece(square)
case piece.kind:
of King:
return getKingAttacks(square)
of Knight:
return getKnightAttacks(square)
of Queen, Rook, Bishop:
let occupancy = position.getOccupancy()
if piece.kind in [Rook, Queen]:
let blockers = occupancy and Rook.getRelevantBlockers(square)
result = getRookMoves(square, blockers)
if piece.kind in [Bishop, Queen]:
let blockers = occupancy and Bishop.getRelevantBlockers(square)
result = result or getBishopMoves(square, blockers)
of Pawn:
return getPawnAttacks(piece.color, square)
else:
discard
proc evaluate*(position: Position, mode: static EvalMode, features: Features = Features()): Score =
## Evaluates the current position
let
sideToMove = position.sideToMove
nonSideToMove = sideToMove.opposite()
middleGamePhase = position.getGamePhase()
occupancy = position.getOccupancy()
pawns: array[PieceColor.White..PieceColor.Black, Bitboard] = [position.getBitboard(Pawn, White), position.getBitboard(Pawn, Black)]
rooks: array[PieceColor.White..PieceColor.Black, Bitboard] = [position.getBitboard(Rook, White), position.getBitboard(Rook, Black)]
queens: array[PieceColor.White..PieceColor.Black, Bitboard] = [position.getBitboard(Queen, White), position.getBitboard(Queen, Black)]
bishops: array[PieceColor.White..PieceColor.Black, Bitboard] = [position.getBitboard(Bishop, White), position.getBitboard(Bishop, Black)]
knights: array[PieceColor.White..PieceColor.Black, Bitboard] = [position.getBitboard(Knight, White), position.getBitboard(Knight, Black)]
majors: array[PieceColor.White..PieceColor.Black, Bitboard] = [queens[White] or rooks[White], queens[Black] or rooks[Black]]
minors: array[PieceColor.White..PieceColor.Black, Bitboard] = [bishops[White] or knights[White], bishops[Black] or knights[Black]]
kingZones: array[PieceColor.White..PieceColor.Black, Bitboard] = [getKingZoneMask(White, position.getBitboard(King, White).toSquare()),
getKingZoneMask(Black, position.getBitboard(King, Black).toSquare())]
allPawns = pawns[White] or pawns[Black]
endGamePhase = 24 - middleGamePhase
scaledMiddleGame = middleGamePhase / 24
scaledEndGame = endGamePhase / 24
var
middleGameScores: array[PieceColor.White..PieceColor.Black, Score] = [0, 0]
endGameScores: array[PieceColor.White..PieceColor.Black, Score] = [0, 0]
kingAttackers: array[PieceColor.White..PieceColor.Black, int] = [0, 0]
# Material, position, threat and mobility evaluation
for sq in occupancy:
let piece = position.getPiece(sq)
let enemyColor = piece.color.opposite()
let attacks = position.getAttackingMoves(sq)
let attacksOnMinors = (attacks and minors[enemyColor]).countSquares()
let attacksOnMajors = (attacks and majors[enemyColor]).countSquares()
let attacksOnQueens = (attacks and queens[enemyColor]).countSquares()
kingAttackers[enemyColor] += (attacks and kingZones[enemyColor]).countSquares()
let mobilityMoves = position.getMobility(sq).countSquares()
when mode == Default:
middleGameScores[piece.color] += MIDDLEGAME_VALUE_TABLES[piece.color][piece.kind][sq]
endGameScores[piece.color] += ENDGAME_VALUE_TABLES[piece.color][piece.kind][sq]
let scores = piece.kind.getMobilityBonus(mobilityMoves)
middleGameScores[piece.color] += scores.mg
endGameScores[piece.color] += scores.eg
case piece.kind:
of Bishop, Knight:
middleGameScores[piece.color] += MINOR_THREATS_MAJOR_BONUS.mg * Score(attacksOnMajors)
endGameScores[piece.color] += MINOR_THREATS_MAJOR_BONUS.eg * Score(attacksOnMajors)
of Pawn:
middleGameScores[piece.color] += PAWN_THREATS_MAJOR_BONUS.mg * Score(attacksOnMajors)
endGameScores[piece.color] += PAWN_THREATS_MAJOR_BONUS.eg * Score(attacksOnMajors)
middleGameScores[piece.color] += PAWN_THREATS_MINOR_BONUS.mg * Score(attacksOnMinors)
endGameScores[piece.color] += PAWN_THREATS_MINOR_BONUS.eg * Score(attacksOnMinors)
of Rook:
middleGameScores[piece.color] += ROOK_THREATS_QUEEN_BONUS.mg * Score(attacksOnQueens)
endGameScores[piece.color] += ROOK_THREATS_QUEEN_BONUS.eg * Score(attacksOnQueens)
else:
discard
else:
# The target square for the piece square tables depends on
# color, so we flip it for black
let square = if piece.color == Black: sq.flip() else: sq
let side = if piece.color == Black: -1.0 else: 1.0
# PSQTs
features.psqts[piece.kind][square].mg += scaledMiddleGame * side
features.psqts[piece.kind][square].eg += scaledEndGame * side
features.pieceWeights[piece.kind].mg += scaledMiddleGame * side
features.pieceWeights[piece.kind].eg += scaledEndGame * side
# Mobility and threats
case piece.kind:
of Bishop:
features.bishopMobility[mobilityMoves].mg += scaledMiddleGame * side
features.bishopMobility[mobilityMoves].eg += scaledEndGame * side
#[
features.minorMajorThreats.mg += side * attacksOnMajors.float * scaledMiddleGame
features.minorMajorThreats.eg += side * attacksOnMajors.float * scaledEndGame
]#
of Knight:
features.knightMobility[mobilityMoves].mg += scaledMiddleGame * side
features.knightMobility[mobilityMoves].eg += scaledEndGame * side
#[
features.minorMajorThreats.mg += side * attacksOnMajors.float * scaledMiddleGame
features.minorMajorThreats.eg += side * attacksOnMajors.float * scaledEndGame
]#
of Rook:
features.rookMobility[mobilityMoves].mg += scaledMiddleGame * side
features.rookMobility[mobilityMoves].eg += scaledEndGame * side
#[
features.rookQueenThreats.mg += side * attacksOnQueens.float * scaledMiddleGame
features.rookQueenThreats.eg += side * attacksOnQueens.float * scaledEndGame
]#
of Queen:
features.queenMobility[mobilityMoves].mg += scaledMiddleGame * side
features.queenMobility[mobilityMoves].eg += scaledEndGame * side
of King:
features.virtualQueenMobility[mobilityMoves].mg += scaledMiddleGame * side
features.virtualQueenMobility[mobilityMoves].eg += scaledEndGame * side
of Pawn:
#[
features.pawnMinorThreats.mg += side * attacksOnMinors.float * scaledMiddleGame
features.pawnMinorThreats.eg += side * attacksOnMinors.float * scaledEndGame
features.pawnMajorThreats.mg += side * attacksOnMajors.float * scaledMiddleGame
features.pawnMajorThreats.eg += side * attacksOnMajors.float * scaledEndGame
]#
discard
else:
discard
for color in PieceColor.White..PieceColor.Black:
let side = if color == Black: -1.0 else: 1.0
# We only count positions with exactly two bishops because
# giving a bonus to a position with an underpromotion to a
# bishop seems silly. Also, we don't actually check that the
# bishops are on different colored squares because having two
# same colored bishops is quite rare and checking that would
# be needlessly expensive for the vast majority of cases
let bishopPair = position.getBitboard(Bishop, color).countSquares() == 2
# Bishop pair
when mode == Default:
if bishopPair:
middleGameScores[color] += BISHOP_PAIR_BONUS.mg
endGameScores[color] += BISHOP_PAIR_BONUS.eg
else:
if bishopPair:
features.bishopPair.mg += side * scaledMiddleGame
features.bishopPair.eg += side * scaledEndGame
# Connected rooks
#[if rooks[color].countSquares() == 2:
# Like the bishop pair bonus, we only give a bonus
# for connected rooks when there's exactly two rooks
# for pretty much the same reasons
let rooksAreConnected = (occupancy and getRayBetween(rooks[color].lowestSquare(), rooks[color].highestSquare())) == 0
when mode == Default:
if rooksAreConnected:
middleGameScores[color] += CONNECTED_ROOKS_BONUS[0]
endGameScores[color] += CONNECTED_ROOKS_BONUS[1]
else:
if rooksAreConnected:
features.connectedRooks.mg += side * scaledMiddleGame
features.connectedRooks.eg += side * scaledEndGame
]#
# King zone attacks
# We clamp the number of attacks we count in the king zone, for our own sanity
let attacked = clamp(kingAttackers[color], 0, features.kingZoneAttacks.high())
when mode == Default:
middleGameScores[color] += KING_ZONE_ATTACKS_MIDDLEGAME_BONUS[attacked]
endGameScores[color] += KING_ZONE_ATTACKS_ENDGAME_BONUS[attacked]
else:
features.kingZoneAttacks[attacked].mg += scaledMiddleGame * side
features.kingZoneAttacks[attacked].eg += scaledEndGame * side
# Pawn structure
# Strong pawns
let strongPawns = ((pawns[color].forwardLeftRelativeTo(color) or pawns[color].forwardRightRelativeTo(color)) and pawns[color]).countSquares()
when mode == Default:
middleGameScores[color] += STRONG_PAWNS_BONUS.mg * Score(strongPawns)
endGameScores[color] += STRONG_PAWNS_BONUS.eg * Score(strongPawns)
else:
features.strongPawns.mg += strongPawns.float * side * scaledMiddleGame
features.strongPawns.eg += strongPawns.float * side * scaledEndGame
for pawn in pawns[color]:
let square = if color == Black: pawn.flip() else: pawn
# Passed pawns
if (getPassedPawnMask(color, pawn) and pawns[color.opposite()]) == 0:
when mode == Default:
middleGameScores[color] += PASSED_PAWN_MIDDLEGAME_TABLES[color][pawn]
endGameScores[color] += PASSED_PAWN_ENDGAME_TABLES[color][pawn]
else:
features.passedPawnBonuses[square].mg += scaledMiddleGame * side
features.passedPawnBonuses[square].eg += scaledEndGame * side
# Isolated pawns
if (pawns[color] and getIsolatedPawnMask(fileFromSquare(pawn))) == 0:
when mode == Default:
middleGameScores[color] += ISOLATED_PAWN_MIDDLEGAME_TABLES[color][pawn]
endGameScores[color] += ISOLATED_PAWN_ENDGAME_TABLES[color][pawn]
else:
features.isolatedPawnBonuses[square].mg += scaledMiddleGame * side
features.isolatedPawnBonuses[square].eg += scaledEndGame * side
for file in 0..7:
let fileMask = getFileMask(file)
let friendlyPawnsOnFile = pawns[color] and fileMask
# Doubled pawns
#[if friendlyPawnsOnFile != 0:
let doubled = friendlyPawnsOnFile.countSquares() - 1
when mode == Default:
middleGameScores[color] += DOUBLED_PAWNS_BONUS.mg * Score(doubled)
endGameScores[color] += DOUBLED_PAWNS_BONUS.eg * Score(doubled)
else:
features.doubledPawns.mg += doubled.float * scaledMiddleGame * side
features.doubledPawns.eg += doubled.float * scaledEndGame * side
]#
# Rooks on (semi-)open files
if (fileMask and allPawns).countSquares() == 0:
# Open file (no pawns in the way)
for rook in rooks[color] and fileMask:
when mode == Default:
middleGameScores[color] += ROOK_OPEN_FILE_BONUS.mg
endGameScores[color] += ROOK_OPEN_FILE_BONUS.eg
else:
let piece = position.getPiece(rook)
let side = if piece.color == Black: -1.0 else: 1.0
features.rookOpenFile.mg += scaledMiddleGame * side
features.rookOpenFile.eg += scaledEndGame * side
if friendlyPawnsOnFile == 0 and (fileMask and pawns[color.opposite()]).countSquares() == 1:
# Semi-open file (no friendly pawns and only one enemy pawn in the way). We
# deviate from the traditional definition of semi-open file (where any number
# of enemy pawns greater than zero is okay), because it's more likely that a
# position where a rook/queen can capture the only enemy pawn on the file and
# open it are good as opposed to there being 2 or more pawns (which would keep
# the file semi-open even after a capture). Maybe we can investigate different
# definitions and see what works and what doesn't
for rook in rooks[color] and fileMask:
when mode == Default:
middleGameScores[color] += ROOK_SEMI_OPEN_FILE_BONUS.mg
endGameScores[color] += ROOK_SEMI_OPEN_FILE_BONUS.eg
else:
let piece = position.getPiece(rook)
let side = if piece.color == Black: -1.0 else: 1.0
features.rookSemiOpenFile.mg += scaledMiddleGame * side
features.rookSemiOpenFile.eg += scaledEndGame * side
# Final score computation. We interpolate between middle and endgame scores
# according to how many pieces are left on the board
let
middleGameScore = middleGameScores[sideToMove] - middleGameScores[nonSideToMove]
endGameScore = endGameScores[sideToMove] - endGameScores[nonSideToMove]
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
when mode == Default:
# Tempo bonus: gains 19.5 +/- 13.7
result += TEMPO_BONUS
else:
features.tempo = 1.0
func featureCount*(self: Features): int {.exportpy.} =
## Returns the number of features in
## the evaluation
# One weight for tempo
result = 1
# Two PSTQs for each piece in each game phase
result += len(self.psqts[PieceKind.Bishop]) * len(self.psqts) * 2
# Two sets of piece weights for each game phase
result += len(self.pieceWeights) * 2
# Two weights for rooks on open files
result += 2
# Two weights for rooks on semi-open files
result += 2
# Weights for our passed pawn bonuses (one for
# each game phase)
result += len(self.passedPawnBonuses) * 2
# Weights for our isolated pawn bonuses (one for
# each game phase)
result += len(self.isolatedPawnBonuses) * 2
# Weights for piece mobility (one set per phase)
result += len(self.bishopMobility) * 2
result += len(self.knightMobility) * 2
result += len(self.rookMobility) * 2
result += len(self.queenMobility) * 2
result += len(self.virtualQueenMobility) * 2
# Weights for king zone attacks
result += len(self.kingZoneAttacks) * 2
# Flat bonuses for doubled pawns, connected
# rooks, the bishop pair and strong pawns
result += 8
# Flat bonuses for threats
result += 8
proc reset(self: Features) =
## Resets the feature metadata
for kind in PieceKind.Bishop..PieceKind.Rook:
self.pieceWeights[kind] = (0, 0)
for square in Square(0)..Square(63):
self.psqts[kind][square] = (0, 0)
self.tempo = 0
self.rookOpenFile = (0, 0)
self.rookSemiOpenFile = (0, 0)
for square in Square(0)..Square(63):
self.passedPawnBonuses[square] = (0, 0)
self.isolatedPawnBonuses[square] = (0, 0)
for i in 0..self.bishopMobility.high():
self.bishopMobility[i] = (0, 0)
for i in 0..self.knightMobility.high():
self.knightMobility[i] = (0, 0)
for i in 0..self.rookMobility.high():
self.rookMobility[i] = (0, 0)
for i in 0..self.queenMobility.high():
self.queenMobility[i] = (0, 0)
for i in 0..self.virtualQueenMobility.high():
self.virtualQueenMobility[i] = (0, 0)
for i in 0..self.kingZoneAttacks.high():
self.kingZoneAttacks[i] = (0, 0)
self.doubledPawns = (0, 0)
self.connectedRooks = (0, 0)
self.bishopPair = (0, 0)
self.strongPawns = (0, 0)
self.pawnMajorThreats = (0, 0)
self.pawnMinorThreats = (0, 0)
self.minorMajorThreats = (0, 0)
self.rookQueenThreats = (0, 0)
proc extract*(self: Features, fen: string): Tensor[float] =
## Extracts the features of the evaluation
## into a 1-D column vector to be used for
## tuning purposes
# In order to avoid messing our tuning by carrying
# over data from previously analyzed positions, we
# zero the metadata at every call to extract
self.reset()
var position = loadFEN(fen)
result = newTensor[float](1, self.featureCount())
discard position.evaluate(EvalMode.Tune, self)
for kind in PieceKind.Bishop..PieceKind.Rook:
for square in Square(0)..Square(63):
var idx = kind.int * len(self.psqts[kind]) + square.int
# All middle-game weights come first, then all engdame ones
result[0, idx] = self.psqts[kind][square].mg
# Skip to the corresponding endgame entry
idx += 64 * 6
result[0, idx] = self.psqts[kind][square].eg
# Skip the piece-square tables
var offset = 64 * 6 * 2
for kind in PieceKind.Bishop..PieceKind.Rook:
var idx = offset + kind.int
result[0, idx] = self.pieceWeights[kind].mg
# Skip to the corresponding end-game piece weight entry
idx += 6
result[0, idx] = self.pieceWeights[kind].eg
offset += 12
# Bonuses for rooks on (semi-)open files
result[0, offset] = self.rookOpenFile.mg
result[0, offset + 1] = self.rookOpenFile.eg
result[0, offset + 2] = self.rookSemiOpenFile.mg
result[0, offset + 3] = self.rookSemiOpenFile.eg
offset += 4
# Bonuses for passed pawns
for square in Square(0)..Square(63):
var idx = square.int + offset
result[0, idx] = self.passedPawnBonuses[square].mg
idx += 64
result[0, idx] = self.passedPawnBonuses[square].eg
offset += 128
# "Bonuses" for isolated pawns
for square in Square(0)..Square(63):
var idx = square.int + offset
result[0, idx] = self.isolatedPawnBonuses[square].mg
idx += 64
result[0, idx] = self.isolatedPawnBonuses[square].eg
offset += 128
# Mobility bonuses
# Bishops
for i in 0..self.bishopMobility.high():
let idx = offset + i * 2
result[0, idx] = self.bishopMobility[i].mg
result[0, idx + 1] = self.bishopMobility[i].eg
offset += len(self.bishopMobility) * 2
# Knights
for i in 0..self.knightMobility.high():
let idx = offset + i * 2
result[0, idx] = self.knightMobility[i].mg
result[0, idx + 1] = self.knightMobility[i].eg
offset += len(self.knightMobility) * 2
# Rooks
for i in 0..self.rookMobility.high():
let idx = offset + i * 2
result[0, idx] = self.rookMobility[i].mg
result[0, idx + 1] = self.rookMobility[i].eg
offset += len(self.rookMobility) * 2
# Queens
for i in 0..self.queenMobility.high():
let idx = offset + i * 2
result[0, idx] = self.queenMobility[i].mg
result[0, idx + 1] = self.queenMobility[i].eg
offset += len(self.queenMobility) * 2
# King
for i in 0..self.virtualQueenMobility.high():
let idx = offset + i * 2
result[0, idx] = self.virtualQueenMobility[i].mg
result[0, idx + 1] = self.virtualQueenMobility[i].eg
offset += len(self.virtualQueenMobility) * 2
# King zone attacks
for i in 0..self.kingZoneAttacks.high():
let idx = offset + i * 2
result[0, idx] = self.kingZoneAttacks[i].mg
result[0, idx + 1] = self.kingZoneAttacks[i].eg
offset += len(self.kingZoneAttacks) * 2
# Doubled pawns maluses
result[0, offset] = self.doubledPawns.mg
result[0, offset + 1] = self.doubledPawns.eg
offset += 2
# Bishop pair bonuses
result[0, offset] = self.bishopPair.mg
result[0, offset + 1] = self.bishopPair.eg
offset += 2
# Connected rooks bonuses
result[0, offset] = self.connectedRooks.mg
result[0, offset + 1] = self.connectedRooks.eg
offset += 2
# Strong pawn bonuses
result[0, offset] = self.strongPawns.mg
result[0, offset + 1] = self.strongPawns.eg
offset += 2
# Threats
result[0, offset] = self.pawnMinorThreats.mg
result[0, offset + 1] = self.pawnMinorThreats.eg
offset += 2
result[0, offset] = self.pawnMajorThreats.mg
result[0, offset + 1] = self.pawnMajorThreats.eg
offset += 2
result[0, offset] = self.minorMajorThreats.mg
result[0, offset + 1] = self.minorMajorThreats.eg
offset += 2
result[0, offset] = self.rookQueenThreats.mg
result[0, offset + 1] = self.rookQueenThreats.eg
offset += 2
# Tempo is always last in the feature vector
result[0, ^1] = self.tempo
proc extractFeatures*(self: Features, fen: string): auto {.exportpy.} =
## Version of extract() exported to Python that returns
## a numpy array
result = self.extract(fen).toNdArray()

View File

@ -0,0 +1,380 @@
# 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.
## Low-level magic bitboard stuff
# Blatantly stolen from this amazing article: https://analog-hors.github.io/site/magic-bitboards/
import bitboards
import pieces
import std/random
import std/bitops
import std/tables
import std/os
import jsony
export pieces
export bitboards
type
MagicEntry = object
## A magic bitboard entry
mask: Bitboard
value: uint64
shift: uint8
# Yeah uh, don't look too closely at this...
proc generateRookBlockers: array[64, Bitboard] {.compileTime.} =
## Generates all blocker masks for rooks
for rank in 0..7:
for file in 0..7:
let
square = makeSquare(rank, file)
i = square.int
bitboard = square.toBitboard()
var
current = bitboard
last = makeSquare(rank, 7).toBitboard()
while true:
current = current.rightRelativeTo(White)
if current == last or current == 0:
break
result[i] = result[i] or current
current = bitboard
last = makeSquare(rank, 0).toBitboard()
while true:
current = current.leftRelativeTo(White)
if current == last or current == 0:
break
result[i] = result[i] or current
current = bitboard
last = makeSquare(0, file).toBitboard()
while true:
current = current.forwardRelativeTo(White)
if current == last or current == 0:
break
result[i] = result[i] or current
current = bitboard
last = makeSquare(7, file).toBitboard()
while true:
current = current.backwardRelativeTo(White)
if current == last or current == 0: