Compare commits
35 Commits
48e2adddc6
...
23d7a0427f
Author | SHA1 | Date |
---|---|---|
Mattia Giambirtone | 23d7a0427f | |
Mattia Giambirtone | d03b2c2fbf | |
Mattia Giambirtone | e62c78e4cc | |
Mattia Giambirtone | 0dfd647f4c | |
Mattia Giambirtone | 04bfe74ad5 | |
Mattia Giambirtone | 68c170568e | |
Mattia Giambirtone | 1d6c74611b | |
Mattia Giambirtone | 4404ce10b9 | |
Mattia Giambirtone | c072576b23 | |
Mattia Giambirtone | fe987576c3 | |
Mattia Giambirtone | d5bcd15c48 | |
Mattia Giambirtone | 4a9deb517a | |
Mattia Giambirtone | f5135ef69e | |
Mattia Giambirtone | 9528fb9849 | |
Mattia Giambirtone | 77ff697df7 | |
Mattia Giambirtone | 2b16b5ec61 | |
Mattia Giambirtone | 6fbcd4ff74 | |
Mattia Giambirtone | 64c30b8a90 | |
Mattia Giambirtone | 0496047164 | |
Mattia Giambirtone | fcbe15f275 | |
Mattia Giambirtone | 19ad46bbda | |
Mattia Giambirtone | 82cef11cc4 | |
Mattia Giambirtone | 6a548bf372 | |
Mattia Giambirtone | 244ad1725a | |
Mattia Giambirtone | a07e9cc475 | |
Mattia Giambirtone | 3bb2cc7c66 | |
Mattia Giambirtone | 86265c68f0 | |
Mattia Giambirtone | e50cfb9d64 | |
Mattia Giambirtone | 3299f09e1f | |
Mattia Giambirtone | b5181317ef | |
Mattia Giambirtone | e1ccdc728e | |
Mattia Giambirtone | c9988cd939 | |
Mattia Giambirtone | 6115191ed6 | |
Mattia Giambirtone | 75d93a0d59 | |
Mattia Giambirtone | 7a885b65a0 |
|
@ -2,7 +2,6 @@
|
|||
nimcache/
|
||||
nimblecache/
|
||||
htmldocs/
|
||||
nim.cfg
|
||||
bin
|
||||
# Python
|
||||
__pycache__
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# A chess engine written in nim
|
||||
|
||||
For now, that's about it.
|
||||
|
||||
|
||||
# Installation
|
||||
|
||||
Just run `nimble install`
|
|
@ -0,0 +1,5 @@
|
|||
--cc:clang
|
||||
-o:"bin/nimfish"
|
||||
-d:danger
|
||||
--passL:"-flto"
|
||||
--passC:"-Ofast -flto -march=native -mtune=native"
|
|
@ -0,0 +1,20 @@
|
|||
# Package
|
||||
|
||||
version = "0.1.0"
|
||||
author = "nocturn9x"
|
||||
description = "A chess engine written in nim"
|
||||
license = "Apache-2.0"
|
||||
srcDir = "nimfish"
|
||||
binDir = "bin"
|
||||
installExt = @["nim"]
|
||||
bin = @["nimfish"]
|
||||
|
||||
|
||||
# Dependencies
|
||||
|
||||
requires "nim >= 2.0"
|
||||
requires "jsony >= 1.1.5"
|
||||
|
||||
|
||||
task test, "Runs the test suite":
|
||||
exec "python tests/suite.py -d 6 --bulk"
|
|
@ -0,0 +1,35 @@
|
|||
# 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 nimfishpkg/tui
|
||||
import nimfishpkg/misc
|
||||
import nimfishpkg/movegen
|
||||
import nimfishpkg/bitboards
|
||||
import nimfishpkg/moves
|
||||
import nimfishpkg/pieces
|
||||
import nimfishpkg/magics
|
||||
import nimfishpkg/rays
|
||||
import nimfishpkg/position
|
||||
import nimfishpkg/board
|
||||
|
||||
|
||||
export tui, misc, movegen, bitboards, moves, pieces, magics, rays, position, board
|
||||
|
||||
|
||||
when isMainModule:
|
||||
basicTests()
|
||||
|
||||
setControlCHook(proc () {.noconv.} = quit(0))
|
||||
|
||||
quit(commandLoop())
|
|
@ -0,0 +1,311 @@
|
|||
# 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 = Bitboard(a.uint64 shl x)
|
||||
func `shr`*(a: Bitboard, x: Natural): Bitboard = Bitboard(a.uint64 shr x)
|
||||
func `and`*(a, b: Bitboard): Bitboard = Bitboard(a.uint64 and b.uint64)
|
||||
func `or`*(a, b: Bitboard): Bitboard = Bitboard(a.uint64 or b.uint64)
|
||||
func `not`*(a: Bitboard): Bitboard = Bitboard(not a.uint64)
|
||||
func `shr`*(a, b: Bitboard): Bitboard = Bitboard(a.uint64 and b.uint64)
|
||||
func `xor`*(a, b: Bitboard): Bitboard = Bitboard(a.uint64 xor b.uint64)
|
||||
func `+`*(a, b: Bitboard): Bitboard = Bitboard(a.uint64 + b.uint64)
|
||||
func `-`*(a, b: Bitboard): Bitboard = Bitboard(a.uint64 - b.uint64)
|
||||
func `div`*(a, b: Bitboard): Bitboard = Bitboard(a.uint64 div b.uint64)
|
||||
func `*`*(a, b: Bitboard): Bitboard = Bitboard(a.uint64 * b.uint64)
|
||||
func `+`*(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 + b)
|
||||
func `-`*(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 - b.uint64)
|
||||
func `div`*(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 div b)
|
||||
func `*`*(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 * b)
|
||||
func `*`*(a: SomeUnsignedInt, b: Bitboard): Bitboard = Bitboard(a.uint64 * b)
|
||||
func `==`*(a, b: Bitboard): bool {.inline.} = a.uint64 == b.uint64
|
||||
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 = a.uint64.countLeadingZeroBits()
|
||||
func countTrailingZeroBits*(a: Bitboard): int = a.uint64.countTrailingZeroBits()
|
||||
func clearBit*(a: var Bitboard, bit: SomeInteger) = a.uint64.clearBit(bit)
|
||||
func setBit*(a: var Bitboard, bit: SomeInteger) = a.uint64.setBit(bit)
|
||||
func clearBit*(a: var Bitboard, bit: Square) = a.uint64.clearBit(bit.int)
|
||||
func setBit*(a: var Bitboard, bit: Square) = a.uint64.setBit(bit.int)
|
||||
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 getFileMask*(file: int): Bitboard = Bitboard(0x101010101010101'u64) shl file.uint64
|
||||
func getRankMask*(rank: int): Bitboard = Bitboard(0xff) shl uint64(8 * rank)
|
||||
func toBitboard*(square: SomeInteger): Bitboard = Bitboard(1'u64) shl square.uint64
|
||||
func toBitboard*(square: Square): Bitboard = toBitboard(square.int8)
|
||||
|
||||
proc toSquare*(b: Bitboard): Square = Square(b.uint64.countTrailingZeroBits())
|
||||
|
||||
func createMove*(startSquare: Bitboard, targetSquare: Square, flags: varargs[MoveFlag]): Move =
|
||||
result = createMove(startSquare.toSquare(), targetSquare, flags)
|
||||
|
||||
|
||||
func createMove*(startSquare: Square, targetSquare: Bitboard, flags: varargs[MoveFlag]): Move =
|
||||
result = createMove(startSquare, targetSquare.toSquare(), flags)
|
||||
|
||||
|
||||
func createMove*(startSquare, targetSquare: Bitboard, flags: varargs[MoveFlag]): Move =
|
||||
result = createMove(startSquare.toSquare(), targetSquare.toSquare(), flags)
|
||||
|
||||
|
||||
func toBin*(x: Bitboard, b: Positive = 64): string = toBin(BiggestInt(x), b)
|
||||
func toBin*(x: uint64, b: Positive = 64): string = toBin(Bitboard(x), b)
|
||||
|
||||
func contains*(self: Bitboard, square: Square): bool = (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 = 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 ForwardLeft:
|
||||
return bitboard shl 9
|
||||
of ForwardRight:
|
||||
return bitboard shl 7
|
||||
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 getLastRank*(color: PieceColor): Bitboard = (if color == White: getRankMask(0) else: getRankMask(7))
|
||||
func getFirstRank*(color: PieceColor): Bitboard = (if color == White: getRankMask(7) else: getRankMask(0))
|
||||
func getLeftmostFile*(color: PieceColor): Bitboard = (if color == White: getFileMask(0) else: getFileMask(7))
|
||||
func getRightmostFile*(color: PieceColor): Bitboard = (if color == White: getFileMask(7) else: getFileMask(0))
|
||||
|
||||
|
||||
func getDirectionMask*(square: Square, color: PieceColor, direction: Direction): Bitboard =
|
||||
## 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 = getDirectionMask(self, side, Forward)
|
||||
func doubleForwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.forwardRelativeTo(side).forwardRelativeTo(side)
|
||||
|
||||
func backwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Backward)
|
||||
func doubleBackwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.backwardRelativeTo(side).backwardRelativeTo(side)
|
||||
|
||||
func leftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Left) and not getRightmostFile(side)
|
||||
func rightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Right) and not getLeftmostFile(side)
|
||||
|
||||
|
||||
# We mask off the opposide files to make sure there are
|
||||
# no weird wraparounds when moving diagonally
|
||||
func forwardRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard =
|
||||
getDirectionMask(self, side, ForwardRight) and not getLeftmostFile(side)
|
||||
|
||||
|
||||
func forwardLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard =
|
||||
getDirectionMask(self, side, ForwardLeft) and not getRightmostFile(side)
|
||||
|
||||
|
||||
func backwardRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard =
|
||||
getDirectionMask(self, side, BackwardRight) and not getLeftmostFile(side)
|
||||
|
||||
|
||||
func backwardLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard =
|
||||
getDirectionMask(self, side, BackwardLeft) and not getRightmostFile(side)
|
||||
|
||||
|
||||
func longKnightUpLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.doubleForwardRelativeTo(side).leftRelativeTo(side)
|
||||
func longKnightUpRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.doubleForwardRelativeTo(side).rightRelativeTo(side)
|
||||
func longKnightDownLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.doubleBackwardRelativeTo(side).leftRelativeTo(side)
|
||||
func longKnightDownRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.doubleBackwardRelativeTo(side).rightRelativeTo(side)
|
||||
|
||||
func shortKnightUpLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.forwardRelativeTo(side).leftRelativeTo(side).leftRelativeTo(side)
|
||||
func shortKnightUpRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.forwardRelativeTo(side).rightRelativeTo(side).rightRelativeTo(side)
|
||||
func shortKnightDownLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.backwardRelativeTo(side).leftRelativeTo(side).leftRelativeTo(side)
|
||||
func shortKnightDownRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.backwardRelativeTo(side).rightRelativeTo(side).rightRelativeTo(side)
|
||||
|
||||
# We precompute as much stuff as possible: lookup tables are fast!
|
||||
|
||||
|
||||
func computeKingBitboards: array[64, Bitboard] {.compileTime.} =
|
||||
## Precomputes all the movement bitboards for the king
|
||||
for i in 0'u64..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[64, Bitboard] {.compileTime.} =
|
||||
## Precomputes all the movement bitboards for knights
|
||||
for i in 0'u64..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
|
||||
|
||||
|
||||
const
|
||||
KING_BITBOARDS = computeKingBitboards()
|
||||
KNIGHT_BITBOARDS = computeKnightBitboards()
|
||||
|
||||
|
||||
func getKingAttacks*(square: Square): Bitboard {.inline.} = KING_BITBOARDS[square.int]
|
||||
func getKnightAttacks*(square: Square): Bitboard {.inline.} = KNIGHT_BITBOARDS[square.int]
|
|
@ -0,0 +1,584 @@
|
|||
# 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 std/strformat
|
||||
import std/strutils
|
||||
|
||||
|
||||
import pieces
|
||||
import magics
|
||||
import moves
|
||||
import rays
|
||||
import bitboards
|
||||
import position
|
||||
|
||||
|
||||
export pieces, position, bitboards, moves, magics, rays
|
||||
|
||||
|
||||
|
||||
type
|
||||
Chessboard* = ref object
|
||||
## A chessboard
|
||||
|
||||
# The actual board where pieces live
|
||||
grid*: array[64, Piece]
|
||||
# The current position
|
||||
position*: Position
|
||||
# List of all previously reached positions
|
||||
positions*: seq[Position]
|
||||
|
||||
|
||||
# A bunch of simple utility functions and forward declarations
|
||||
proc toFEN*(self: Chessboard): string
|
||||
proc updateChecksAndPins*(self: Chessboard)
|
||||
|
||||
|
||||
proc newChessboard: Chessboard =
|
||||
## Returns a new, empty chessboard
|
||||
new(result)
|
||||
for i in 0..63:
|
||||
result.grid[i] = nullPiece()
|
||||
result.position = Position(enPassantSquare: nullSquare(), sideToMove: White)
|
||||
|
||||
|
||||
# Indexing operations
|
||||
func `[]`*(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.int8]
|
||||
func `[]=`*(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.int8] = piece
|
||||
|
||||
|
||||
func getBitboard*(self: Chessboard, kind: PieceKind, color: PieceColor): Bitboard {.inline.} =
|
||||
## Returns the positional bitboard for the given piece kind and color
|
||||
return self.position.getBitboard(kind, color)
|
||||
|
||||
|
||||
func getBitboard*(self: Chessboard, piece: Piece): Bitboard {.inline.} =
|
||||
## Returns the positional bitboard for the given piece type
|
||||
return self.getBitboard(piece.kind, piece.color)
|
||||
|
||||
|
||||
proc newChessboardFromFEN*(fen: string): Chessboard =
|
||||
## Initializes a chessboard with the
|
||||
## position encoded by the given FEN string
|
||||
result = newChessboard()
|
||||
var
|
||||
# Current square in the grid
|
||||
row: int8 = 0
|
||||
column: int8 = 0
|
||||
# Current section in the FEN string
|
||||
section = 0
|
||||
# Current index into the FEN string
|
||||
index = 0
|
||||
# Temporary variable to store a piece
|
||||
piece: Piece
|
||||
# See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation
|
||||
while index <= fen.high():
|
||||
var c = fen[index]
|
||||
if c == ' ':
|
||||
# Next section
|
||||
inc(section)
|
||||
inc(index)
|
||||
continue
|
||||
case section:
|
||||
of 0:
|
||||
# Piece placement data
|
||||
case c.toLowerAscii():
|
||||
# Piece
|
||||
of 'r', 'n', 'b', 'q', 'k', 'p':
|
||||
let square = makeSquare(row, column)
|
||||
piece = c.fromChar()
|
||||
result.position.pieces[piece.color][piece.kind][].setBit(square)
|
||||
result.grid[square] = piece
|
||||
inc(column)
|
||||
of '/':
|
||||
# Next row
|
||||
inc(row)
|
||||
column = 0
|
||||
of '0'..'9':
|
||||
# Skip x columns
|
||||
let x = int(uint8(c) - uint8('0'))
|
||||
if x > 8:
|
||||
raise newException(ValueError, &"invalid FEN: invalid column skip size ({x} > 8)")
|
||||
column += int8(x)
|
||||
else:
|
||||
raise newException(ValueError, &"invalid FEN: unknown piece identifier '{c}'")
|
||||
of 1:
|
||||
# Active color
|
||||
case c:
|
||||
of 'w':
|
||||
result.position.sideToMove = White
|
||||
of 'b':
|
||||
result.position.sideToMove = Black
|
||||
else:
|
||||
raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'")
|
||||
of 2:
|
||||
# Castling availability
|
||||
case c:
|
||||
# TODO
|
||||
of '-':
|
||||
discard
|
||||
of 'K':
|
||||
result.position.castlingAvailability[White.int].king = true
|
||||
of 'Q':
|
||||
result.position.castlingAvailability[White.int].queen = true
|
||||
of 'k':
|
||||
result.position.castlingAvailability[Black.int].king = true
|
||||
of 'q':
|
||||
result.position.castlingAvailability[Black.int].queen = true
|
||||
else:
|
||||
raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights availability section")
|
||||
of 3:
|
||||
# En passant target square
|
||||
case c:
|
||||
of '-':
|
||||
# Field is already uninitialized to the correct state
|
||||
discard
|
||||
else:
|
||||
result.position.enPassantSquare = fen[index..index+1].toSquare()
|
||||
# Square metadata is 2 bytes long
|
||||
inc(index)
|
||||
of 4:
|
||||
# Halfmove clock
|
||||
var s = ""
|
||||
while not fen[index].isSpaceAscii():
|
||||
s.add(fen[index])
|
||||
inc(index)
|
||||
# Backtrack so the space is seen by the
|
||||
# next iteration of the loop
|
||||
dec(index)
|
||||
result.position.halfMoveClock = parseInt(s).int8
|
||||
of 5:
|
||||
# Fullmove number
|
||||
var s = ""
|
||||
while index <= fen.high():
|
||||
s.add(fen[index])
|
||||
inc(index)
|
||||
result.position.fullMoveCount = parseInt(s).int8
|
||||
else:
|
||||
raise newException(ValueError, "invalid FEN: too many fields in FEN string")
|
||||
inc(index)
|
||||
result.updateChecksAndPins()
|
||||
|
||||
|
||||
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 countPieces*(self: Chessboard, kind: PieceKind, color: PieceColor): int {.inline.} =
|
||||
## Returns the number of pieces with
|
||||
## the given color and type in the
|
||||
## current position
|
||||
return self.position.pieces[color][kind][].countSquares()
|
||||
|
||||
|
||||
func getPiece*(self: Chessboard, square: Square): Piece {.inline.} =
|
||||
## Gets the piece at the given square
|
||||
return self.grid[square]
|
||||
|
||||
|
||||
func getPiece*(self: Chessboard, square: string): Piece {.inline.} =
|
||||
## Gets the piece on the given square
|
||||
## in algebraic notation
|
||||
return self.getPiece(square.toSquare())
|
||||
|
||||
|
||||
proc removePieceFromBitboard*(self: Chessboard, square: Square) =
|
||||
## Removes a piece at the given square in the chessboard from
|
||||
## its respective bitboard
|
||||
let piece = self.getPiece(square)
|
||||
self.position.pieces[piece.color][piece.kind][].clearBit(square)
|
||||
|
||||
|
||||
proc addPieceToBitboard*(self: Chessboard, square: Square, piece: Piece) =
|
||||
## Adds the given piece at the given square in the chessboard to
|
||||
## its respective bitboard
|
||||
self.position.pieces[piece.color][piece.kind][].setBit(square)
|
||||
|
||||
|
||||
proc spawnPiece*(self: Chessboard, square: Square, piece: Piece) =
|
||||
## Internal helper to "spawn" a given piece at the given
|
||||
## square
|
||||
when not defined(danger):
|
||||
doAssert self.getPiece(square).kind == Empty
|
||||
self.addPieceToBitboard(square, piece)
|
||||
self.grid[square] = piece
|
||||
|
||||
|
||||
proc removePiece*(self: Chessboard, square: Square) =
|
||||
## Removes a piece from the board, updating necessary
|
||||
## metadata
|
||||
when not defined(danger):
|
||||
let Piece = self.getPiece(square)
|
||||
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
|
||||
self.removePieceFromBitboard(square)
|
||||
self.grid[square] = nullPiece()
|
||||
|
||||
|
||||
proc movePiece*(self: Chessboard, move: Move) =
|
||||
## Internal helper to move a piece from
|
||||
## its current square to a target square
|
||||
let piece = self.getPiece(move.startSquare)
|
||||
when not defined(danger):
|
||||
let targetSquare = self.getPiece(move.targetSquare)
|
||||
if targetSquare.color != None:
|
||||
raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}: {move}")
|
||||
# Update positional metadata
|
||||
self.removePiece(move.startSquare)
|
||||
self.spawnPiece(move.targetSquare, piece)
|
||||
|
||||
|
||||
proc movePiece*(self: Chessboard, startSquare, targetSquare: Square) =
|
||||
self.movePiece(createMove(startSquare, targetSquare))
|
||||
|
||||
|
||||
func countPieces*(self: Chessboard, piece: Piece): int {.inline.} =
|
||||
## Returns the number of pieces on the board that
|
||||
## are of the same type and color as the given piece
|
||||
return self.countPieces(piece.kind, piece.color)
|
||||
|
||||
|
||||
func getOccupancyFor*(self: Chessboard, color: PieceColor): Bitboard =
|
||||
## 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
|
||||
|
||||
|
||||
func getOccupancy*(self: Chessboard): Bitboard {.inline.} =
|
||||
## Get the occupancy bitboard for every piece on
|
||||
## the chessboard
|
||||
result = self.getOccupancyFor(Black) or self.getOccupancyFor(White)
|
||||
|
||||
|
||||
func getPawnAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} =
|
||||
## Returns the locations of the pawns attacking the given square
|
||||
let
|
||||
sq = square.toBitboard()
|
||||
pawns = self.getBitboard(Pawn, attacker)
|
||||
bottomLeft = sq.backwardLeftRelativeTo(attacker)
|
||||
bottomRight = sq.backwardRightRelativeTo(attacker)
|
||||
return pawns and (bottomLeft or bottomRight)
|
||||
|
||||
|
||||
func getKingAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} =
|
||||
## Returns the location of the king if it is attacking the given square
|
||||
result = Bitboard(0)
|
||||
let
|
||||
king = self.getBitboard(King, attacker)
|
||||
squareBB = square.toBitboard()
|
||||
if (getKingAttacks(square) and squareBB) != 0:
|
||||
result = result or king
|
||||
|
||||
|
||||
func getKnightAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard =
|
||||
## Returns the locations of the knights attacking the given square
|
||||
let
|
||||
knights = self.getBitboard(Knight, attacker)
|
||||
squareBB = square.toBitboard()
|
||||
result = Bitboard(0)
|
||||
for knight in knights:
|
||||
if (getKnightAttacks(knight) and squareBB) != 0:
|
||||
result = result or knight.toBitboard()
|
||||
|
||||
|
||||
proc getSlidingAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard =
|
||||
## Returns the locations of the sliding pieces attacking the given square
|
||||
let
|
||||
queens = self.getBitboard(Queen, attacker)
|
||||
rooks = self.getBitboard(Rook, attacker) or queens
|
||||
bishops = self.getBitboard(Bishop, attacker) or queens
|
||||
occupancy = self.getOccupancy()
|
||||
squareBB = square.toBitboard()
|
||||
result = Bitboard(0)
|
||||
for rook in rooks:
|
||||
let
|
||||
blockers = occupancy and Rook.getRelevantBlockers(rook)
|
||||
moves = getRookMoves(rook, blockers)
|
||||
# Attack set intersects our chosen square
|
||||
if (moves and squareBB) != 0:
|
||||
result = result or rook.toBitboard()
|
||||
for bishop in bishops:
|
||||
let
|
||||
blockers = occupancy and Bishop.getRelevantBlockers(bishop)
|
||||
moves = getBishopMoves(bishop, blockers)
|
||||
if (moves and squareBB) != 0:
|
||||
result = result or bishop.toBitboard()
|
||||
|
||||
|
||||
proc getAttacksTo*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard =
|
||||
## Computes the attack bitboard for the given square from
|
||||
## the given side
|
||||
result = Bitboard(0)
|
||||
result = result or self.getPawnAttacks(square, attacker)
|
||||
result = result or self.getKingAttacks(square, attacker)
|
||||
result = result or self.getKnightAttacks(square, attacker)
|
||||
result = result or self.getSlidingAttacks(square, attacker)
|
||||
|
||||
|
||||
proc isOccupancyAttacked*(self: Chessboard, square: Square, occupancy: Bitboard): bool =
|
||||
## Returns whether the given square would be attacked by the
|
||||
## enemy side if the board had the given occupancy. This function
|
||||
## is necessary mostly to make sure sliding attacks can check the
|
||||
## king properly: due to how we generate our attack bitboards, if
|
||||
## the king moved backwards along a ray from a slider we would not
|
||||
## consider it to be in check (because the ray stops at the first
|
||||
## blocker). In order to fix that, in generateKingMoves() we use this
|
||||
## function and pass in the board's occupancy without the moving king so
|
||||
## that we can pick the correct magic bitboard and ray. Also, since this
|
||||
## function doesn't need to generate all the attacks to know whether a
|
||||
## given square is unsafe, it can short circuit at the first attack and
|
||||
## exit early, unlike getAttacksTo
|
||||
let
|
||||
sideToMove = self.position.sideToMove
|
||||
nonSideToMove = sideToMove.opposite()
|
||||
knights = self.getBitboard(Knight, nonSideToMove)
|
||||
|
||||
# Let's do the cheap ones first (the ones which are precomputed)
|
||||
if (getKnightAttacks(square) and knights) != 0:
|
||||
return true
|
||||
|
||||
let king = self.getBitboard(King, nonSideToMove)
|
||||
|
||||
if (getKingAttacks(square) and king) != 0:
|
||||
return true
|
||||
|
||||
let
|
||||
queens = self.getBitboard(Queen, nonSideToMove)
|
||||
bishops = self.getBitboard(Bishop, nonSideToMove) or queens
|
||||
|
||||
if (getBishopMoves(square, occupancy) and bishops) != 0:
|
||||
return true
|
||||
|
||||
let rooks = self.getBitboard(Rook, nonSideToMove) or queens
|
||||
|
||||
if (getRookMoves(square, occupancy) and rooks) != 0:
|
||||
return true
|
||||
|
||||
# TODO: Precompute pawn moves as well?
|
||||
let pawns = self.getBitboard(Pawn, nonSideToMove)
|
||||
|
||||
if (self.getPawnAttacks(square, nonSideToMove) and pawns) != 0:
|
||||
return true
|
||||
|
||||
|
||||
proc updateChecksAndPins*(self: Chessboard) =
|
||||
## Updates internal metadata about checks and
|
||||
## pinned pieces
|
||||
|
||||
# *Ahem*, stolen from https://github.com/Ciekce/voidstar/blob/424ac4624011271c4d1dbd743602c23f6dbda1de/src/position.rs
|
||||
# Can you tell I'm a *great* coder?
|
||||
let
|
||||
sideToMove = self.position.sideToMove
|
||||
nonSideToMove = sideToMove.opposite()
|
||||
friendlyKing = self.getBitboard(King, sideToMove).toSquare()
|
||||
friendlyPieces = self.getOccupancyFor(sideToMove)
|
||||
enemyPieces = self.getOccupancyFor(nonSideToMove)
|
||||
|
||||
# Update checks
|
||||
self.position.checkers = self.getAttacksTo(friendlyKing, nonSideToMove)
|
||||
# Update pins
|
||||
self.position.diagonalPins = Bitboard(0)
|
||||
self.position.orthogonalPins = Bitboard(0)
|
||||
|
||||
let
|
||||
diagonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Bishop, nonSideToMove)
|
||||
orthogonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Rook, nonSideToMove)
|
||||
canPinDiagonally = diagonalAttackers and getBishopMoves(friendlyKing, enemyPieces)
|
||||
canPinOrthogonally = orthogonalAttackers and getRookMoves(friendlyKing, enemyPieces)
|
||||
|
||||
for piece in canPinDiagonally:
|
||||
let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard()
|
||||
|
||||
# Is the pinning ray obstructed by any of our friendly pieces? If so, the
|
||||
# piece is pinned
|
||||
if (pinningRay and friendlyPieces).countSquares() == 1:
|
||||
self.position.diagonalPins = self.position.diagonalPins or pinningRay
|
||||
|
||||
for piece in canPinOrthogonally:
|
||||
let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard()
|
||||
if (pinningRay and friendlyPieces).countSquares() == 1:
|
||||
self.position.orthogonalPins = self.position.orthogonalPins or pinningRay
|
||||
|
||||
|
||||
func inCheck*(self: Chessboard): bool {.inline.} =
|
||||
## Returns if the current side to move is in check
|
||||
return self.position.checkers != 0
|
||||
|
||||
|
||||
proc canCastle*(self: Chessboard): tuple[queen, king: bool] =
|
||||
## Returns if the current side to move can castle
|
||||
if self.inCheck():
|
||||
return (false, false)
|
||||
let
|
||||
sideToMove = self.position.sideToMove
|
||||
occupancy = self.getOccupancy()
|
||||
result = self.position.castlingAvailability[sideToMove.int]
|
||||
if result.king:
|
||||
result.king = (kingSideCastleRay(sideToMove) and occupancy) == 0
|
||||
if result.queen:
|
||||
result.queen = (queenSideCastleRay(sideToMove) and occupancy) == 0
|
||||
if result.king:
|
||||
# There are no pieces in between our friendly king and
|
||||
# rook: check for attacks
|
||||
let
|
||||
king = self.getBitboard(King, sideToMove).toSquare()
|
||||
for square in getRayBetween(king, sideToMove.kingSideRook()):
|
||||
if self.isOccupancyAttacked(square, occupancy):
|
||||
result.king = false
|
||||
break
|
||||
|
||||
if result.queen:
|
||||
let
|
||||
king: Square = self.getBitboard(King, sideToMove).toSquare()
|
||||
# The king always moves two squares, but the queen side rook moves
|
||||
# 3 squares. We only need to check for attacks on the squares where
|
||||
# the king moves to and not any further. We subtract 3 instead of 2
|
||||
# because getRayBetween ignores the start and target squares in the
|
||||
# ray it returns so we have to extend it by one
|
||||
destination = makeSquare(rankFromSquare(king), fileFromSquare(king) - 3)
|
||||
for square in getRayBetween(king, destination):
|
||||
if self.isOccupancyAttacked(square, occupancy):
|
||||
result.queen = false
|
||||
break
|
||||
|
||||
|
||||
proc update*(self: Chessboard) =
|
||||
## Updates the internal grid representation
|
||||
## according to the positional data stored
|
||||
## in the chessboard
|
||||
for i in 0..63:
|
||||
self.grid[i] = nullPiece()
|
||||
for sq in self.position.pieces[White][Pawn][]:
|
||||
self.grid[sq] = Piece(color: White, kind: Pawn)
|
||||
for sq in self.position.pieces[Black][Pawn][]:
|
||||
self.grid[sq] = Piece(color: Black, kind: Pawn)
|
||||
for sq in self.position.pieces[White][Bishop][]:
|
||||
self.grid[sq] = Piece(color: White, kind: Bishop)
|
||||
for sq in self.position.pieces[Black][Bishop][]:
|
||||
self.grid[sq] = Piece(color: Black, kind: Bishop)
|
||||
for sq in self.position.pieces[White][Knight][]:
|
||||
self.grid[sq] = Piece(color: White, kind: Knight)
|
||||
for sq in self.position.pieces[Black][Knight][]:
|
||||
self.grid[sq] = Piece(color: Black, kind: Knight)
|
||||
for sq in self.position.pieces[White][Rook][]:
|
||||
self.grid[sq] = Piece(color: White, kind: Rook)
|
||||
for sq in self.position.pieces[Black][Rook][]:
|
||||
self.grid[sq] = Piece(color: Black, kind: Rook)
|
||||
for sq in self.position.pieces[White][Queen][]:
|
||||
self.grid[sq] = Piece(color: White, kind: Queen)
|
||||
for sq in self.position.pieces[Black][Queen][]:
|
||||
self.grid[sq] = Piece(color: Black, kind: Queen)
|
||||
for sq in self.position.pieces[White][King][]:
|
||||
self.grid[sq] = Piece(color: White, kind: King)
|
||||
for sq in self.position.pieces[Black][King][]:
|
||||
self.grid[sq] = Piece(color: Black, kind: King)
|
||||
|
||||
|
||||
|
||||
proc `$`*(self: Chessboard): string =
|
||||
result &= "- - - - - - - -"
|
||||
var file = 8
|
||||
for i in 0..7:
|
||||
result &= "\n"
|
||||
for j in 0..7:
|
||||
let piece = self.grid[makeSquare(i, j)]
|
||||
if piece.kind == Empty:
|
||||
result &= "x "
|
||||
continue
|
||||
result &= &"{piece.toChar()} "
|
||||
result &= &"{file}"
|
||||
dec(file)
|
||||
result &= "\n- - - - - - - -"
|
||||
result &= "\na b c d e f g h"
|
||||
|
||||
|
||||
proc pretty*(self: Chessboard): string =
|
||||
## Returns a colored version of the
|
||||
## board for easier visualization
|
||||
var file = 8
|
||||
for i in 0..7:
|
||||
if i > 0:
|
||||
result &= "\n"
|
||||
for j in 0..7:
|
||||
# Equivalent to (i + j) mod 2
|
||||
# (I'm just evil)
|
||||
if ((i + j) and 1) == 0:
|
||||
result &= "\x1b[39;44;1m"
|
||||
else:
|
||||
result &= "\x1b[39;40;1m"
|
||||
let piece = self.grid[makeSquare(i, j)]
|
||||
if piece.kind == Empty:
|
||||
result &= " \x1b[0m"
|
||||
else:
|
||||
result &= &"{piece.toPretty()} \x1b[0m"
|
||||
result &= &" \x1b[33;1m{file}\x1b[0m"
|
||||
dec(file)
|
||||
|
||||
result &= "\n\x1b[31;1ma b c d e f g h"
|
||||
result &= "\x1b[0m"
|
||||
|
||||
|
||||
proc toFEN*(self: Chessboard): string =
|
||||
## Returns a FEN string of the current
|
||||
## position in the chessboard
|
||||
var skip: int
|
||||
# Piece placement data
|
||||
for i in 0..7:
|
||||
skip = 0
|
||||
for j in 0..7:
|
||||
let piece = self.grid[makeSquare(i, j)]
|
||||
if piece.kind == Empty:
|
||||
inc(skip)
|
||||
elif skip > 0:
|
||||
result &= &"{skip}{piece.toChar()}"
|
||||
skip = 0
|
||||
else:
|
||||
result &= piece.toChar()
|
||||
if skip > 0:
|
||||
result &= $skip
|
||||
if i < 7:
|
||||
result &= "/"
|
||||
result &= " "
|
||||
# Active color
|
||||
result &= (if self.position.sideToMove == White: "w" else: "b")
|
||||
result &= " "
|
||||
# Castling availability
|
||||
let castleWhite = self.position.castlingAvailability[White.int]
|
||||
let castleBlack = self.position.castlingAvailability[Black.int]
|
||||
if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen):
|
||||
result &= "-"
|
||||
else:
|
||||
if castleWhite.king:
|
||||
result &= "K"
|
||||
if castleWhite.queen:
|
||||
result &= "Q"
|
||||
if castleBlack.king:
|
||||
result &= "k"
|
||||
if castleBlack.queen:
|
||||
result &= "q"
|
||||
result &= " "
|
||||
# En passant target
|
||||
if self.position.enPassantSquare == nullSquare():
|
||||
result &= "-"
|
||||
else:
|
||||
result &= self.position.enPassantSquare.toAlgebraic()
|
||||
result &= " "
|
||||
# Halfmove clock
|
||||
result &= $self.position.halfMoveClock
|
||||
result &= " "
|
||||
# Fullmove number
|
||||
result &= $self.position.fullMoveCount
|
|
@ -0,0 +1,381 @@
|
|||
# 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:
|
||||
break
|
||||
result[i] = result[i] or current
|
||||
|
||||
|
||||
# Okay this is fucking clever tho. Which is obvious, considering I didn't come up with it.
|
||||
# Or, well, the trick at the end isn't mine
|
||||
func generateBishopBlockers: array[64, Bitboard] {.compileTime.} =
|
||||
## Generates all blocker masks for bishops
|
||||
for rank in 0..7:
|
||||
for file in 0..7:
|
||||
# Generate all possible movement masks
|
||||
let
|
||||
square = makeSquare(rank, file)
|
||||
i = square.int
|
||||
bitboard = square.toBitboard()
|
||||
var
|
||||
current = bitboard
|
||||
while true:
|
||||
current = current.backwardRightRelativeTo(White)
|
||||
if current == 0:
|
||||
break
|
||||
result[i] = result[i] or current
|
||||
current = bitboard
|
||||
while true:
|
||||
current = current.backwardLeftRelativeTo(White)
|
||||
if current == 0:
|
||||
break
|
||||
result[i] = result[i] or current
|
||||
current = bitboard
|
||||
while true:
|
||||
current = current.forwardLeftRelativeTo(White)
|
||||
if current == 0:
|
||||
break
|
||||
result[i] = result[i] or current
|
||||
current = bitboard
|
||||
while true:
|
||||
current = current.forwardRightRelativeTo(White)
|
||||
if current == 0:
|
||||
break
|
||||
result[i] = result[i] or current
|
||||
# Mask off the edges
|
||||
|
||||
# Yeah, this is the trick. I know, not a big deal, but
|
||||
# I'm an idiot so what do I know. Credit to @__arandomnoob
|
||||
# on the engine programming discord server for the tip!
|
||||
result[i] = result[i] and not getFileMask(0)
|
||||
result[i] = result[i] and not getFileMask(7)
|
||||
result[i] = result[i] and not getRankMask(0)
|
||||
result[i] = result[i] and not getRankMask(7)
|
||||
|
||||
|
||||
func getIndex*(magic: MagicEntry, blockers: Bitboard): uint {.inline.} =
|
||||
## Computes an index into the magic bitboard table using
|
||||
## the given magic entry and the blockers bitboard
|
||||
let
|
||||
blockers = blockers and magic.mask
|
||||
hash = blockers * magic.value
|
||||
index = hash shr magic.shift
|
||||
return index.uint
|
||||
|
||||
|
||||
# Magic number tables and their corresponding moves
|
||||
var
|
||||
ROOK_MAGICS: array[64, MagicEntry]
|
||||
ROOK_MOVES: array[64, seq[Bitboard]]
|
||||
BISHOP_MAGICS: array[64, MagicEntry]
|
||||
BISHOP_MOVES: array[64, seq[Bitboard]]
|
||||
|
||||
|
||||
proc getRookMoves*(square: Square, blockers: Bitboard): Bitboard =
|
||||
## Returns the move bitboard for the rook at the given
|
||||
## square with the given blockers bitboard
|
||||
let
|
||||
magic = ROOK_MAGICS[square.uint]
|
||||
movesAddr = addr ROOK_MOVES[square.uint]
|
||||
return movesAddr[][getIndex(magic, blockers)]
|
||||
|
||||
|
||||
proc getBishopMoves*(square: Square, blockers: Bitboard): Bitboard =
|
||||
## Returns the move bitboard for the bishop at the given
|
||||
## square with the given blockers bitboard
|
||||
let
|
||||
magic = BISHOP_MAGICS[square.uint]
|
||||
movesAddr = addr BISHOP_MOVES[square.uint]
|
||||
return movesAddr[][getIndex(magic, blockers)]
|
||||
|
||||
|
||||
proc getRookMagic*(square: Square): MagicEntry =
|
||||
## Returns the magic entry for a rook
|
||||
## on the given square
|
||||
return ROOK_MAGICS[square.uint]
|
||||
|
||||
|
||||
proc getBishopMagic*(square: Square): MagicEntry =
|
||||
## Returns the magic entry for a bishop
|
||||
## on the given square
|
||||
return BISHOP_MAGICS[square.uint]
|
||||
|
||||
|
||||
# Precomputed blocker masks. Only pieces on these bitboards
|
||||
# are actually able to block the movement of a sliding piece,
|
||||
# regardless of color
|
||||
const
|
||||
# mfw Nim's compile time VM *graciously* allows me to call perfectly valid code: :D
|
||||
ROOK_BLOCKERS = generateRookBlockers()
|
||||
BISHOP_BLOCKERS = generateBishopBlockers()
|
||||
|
||||
|
||||
func getRelevantBlockers*(kind: PieceKind, square: Square): Bitboard {.inline.} =
|
||||
## Returns the relevant blockers mask for the given piece
|
||||
## type at the given square
|
||||
case kind:
|
||||
of Rook:
|
||||
return ROOK_BLOCKERS[square.uint]
|
||||
of Bishop:
|
||||
return BISHOP_BLOCKERS[square.uint]
|
||||
else:
|
||||
discard
|
||||
|
||||
# Thanks analog :D
|
||||
const
|
||||
ROOK_DELTAS = [(1, 0), (0, -1), (-1, 0), (0, 1)]
|
||||
BISHOP_DELTAS = [(1, 1), (1, -1), (-1, -1), (-1, 1)]
|
||||
# These are technically (file, rank), but it's all symmetric anyway
|
||||
|
||||
|
||||
func tryOffset(square: Square, df, dr: SomeInteger): Square =
|
||||
let
|
||||
file = fileFromSquare(square)
|
||||
rank = rankFromSquare(square)
|
||||
if file + df notin 0..7:
|
||||
return nullSquare()
|
||||
if rank + dr notin 0..7:
|
||||
return nullSquare()
|
||||
return makeSquare(rank + dr, file + df)
|
||||
|
||||
|
||||
proc getMoveset*(kind: PieceKind, square: Square, blocker: Bitboard): Bitboard =
|
||||
## A naive implementation of sliding attacks. Returns the moves that can
|
||||
## be performed from the given piece at the given square with the given
|
||||
## blocker mask
|
||||
result = Bitboard(0)
|
||||
let deltas = if kind == Rook: ROOK_DELTAS else: BISHOP_DELTAS
|
||||
for (file, rank) in deltas:
|
||||
var ray = square
|
||||
while not blocker.contains(ray):
|
||||
if (let shifted = ray.tryOffset(file, rank); shifted) != nullSquare():
|
||||
ray = shifted
|
||||
result = result or ray.toBitboard()
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
proc attemptMagicTableCreation(kind: PieceKind, square: Square, entry: MagicEntry): tuple[success: bool, table: seq[Bitboard]] =
|
||||
## Tries to create a magic bitboard table for the given piece
|
||||
## at the given square using the provided magic entry. Returns
|
||||
## (true, table) if successful, (false, empty) otherwise
|
||||
|
||||
# Initialize a new sequence with capacity 2^indexBits
|
||||
result.table = newSeqOfCap[Bitboard](1 shl (64'u8 - entry.shift))
|
||||
result.success = true
|
||||
for _ in 0..result.table.capacity:
|
||||
result.table.add(Bitboard(0))
|
||||
# Iterate all possible blocker configurations
|
||||
for blocker in entry.mask.subsets():
|
||||
let index = getIndex(entry, blocker)
|
||||
# Get the moves the piece can make from the given
|
||||
# square with this specific blocker configuration.
|
||||
# Note that this will return the same set of moves
|
||||
# for several different blocker configurations, as
|
||||
# many of them (while different) produce the same
|
||||
# results
|
||||
var moves = kind.getMoveset(square, blocker)
|
||||
if result.table[index] == Bitboard(0):
|
||||
# No entry here, yet, so no problem!
|
||||
result.table[index] = moves
|
||||
elif result.table[index] != moves:
|
||||
# We found a non-constructive collision, fail :(
|
||||
# Notes for future self: A "constructive" collision
|
||||
# is one which doesn't affect the result, because some
|
||||
# blocker configurations will map to the same set of
|
||||
# resulting moves. This actually improves our chances
|
||||
# of building our lovely perfect-hash-function-as-a-table
|
||||
# because we don't actually need to map *all* blocker
|
||||
# configurations uniquely, just the ones that lead to
|
||||
# a different set of moves. This happens because we are
|
||||
# keeping track of a lot of redundant blockers that are
|
||||
# beyond squares a slider piece could go to: we could reduce
|
||||
# the table size if we didn't account for those, but this
|
||||
# would require us to have a loop going in every sliding
|
||||
# direction to find what pieces are actually blocking the
|
||||
# the slider's path and which aren't for every single lookup,
|
||||
# which is the whole thing we're trying to avoid by doing all
|
||||
# this magic bitboard stuff, and it is basically how the old mailbox
|
||||
# move generator worked anyway (thanks to Sebastian Lague on YouTube
|
||||
# for the insight)
|
||||
return (false, @[])
|
||||
# We have found a constructive collision: all good
|
||||
|
||||
|
||||
proc findMagic(kind: PieceKind, square: Square, indexBits: uint8): tuple[entry: MagicEntry, table: seq[Bitboard], iterations: int] =
|
||||
## Constructs a (sort of) perfect hash function that fits all
|
||||
## the possible blocking configurations for the given piece at
|
||||
## the given square into a table of size 2^indexBits
|
||||
let mask = kind.getRelevantBlockers(square)
|
||||
# The best way to find a good magic number? Literally just
|
||||
# bruteforce the shit out of it!
|
||||
var rand = initRand()
|
||||
result.iterations = 0
|
||||
while true:
|
||||
inc(result.iterations)
|
||||
# Again, this is stolen from the article. A magic number
|
||||
# is only useful if it has high bit sparsity, so we AND
|
||||
# together a bunch of random values to get a number that's
|
||||
# hopefully better than a single one
|
||||
let
|
||||
magic = rand.next() and rand.next() and rand.next()
|
||||
entry = MagicEntry(mask: mask, value: magic, shift: 64'u8 - indexBits)
|
||||
var attempt = attemptMagicTableCreation(kind, square, entry)
|
||||
if attempt.success:
|
||||
# Huzzah! Our search for the mighty magic number is complete
|
||||
# (for this square)
|
||||
result.entry = entry
|
||||
result.table = attempt.table
|
||||
return
|
||||
# Not successful? No problem, we'll just try again until
|
||||
# the heat death of the universe! (Not reallty though: finding
|
||||
# magics is pretty fast even if you're unlucky)
|
||||
|
||||
|
||||
proc computeMagics*: int {.discardable.} =
|
||||
## Fills in our magic number tables and returns
|
||||
## the total number of iterations that were performed
|
||||
## to find them
|
||||
for i in 0..63:
|
||||
let square = Square(i)
|
||||
var magic = findMagic(Rook, square, Rook.getRelevantBlockers(square).uint64.countSetBits().uint8)
|
||||
inc(result, magic.iterations)
|
||||
ROOK_MAGICS[i] = magic.entry
|
||||
ROOK_MOVES[i] = magic.table
|
||||
magic = findMagic(Bishop, square, Bishop.getRelevantBlockers(square).uint64.countSetBits().uint8)
|
||||
inc(result, magic.iterations)
|
||||
BISHOP_MAGICS[i] = magic.entry
|
||||
BISHOP_MOVES[i] = magic.table
|
||||
|
||||
|
||||
when isMainModule:
|
||||
import std/strformat
|
||||
import std/strutils
|
||||
import std/times
|
||||
import std/math
|
||||
|
||||
|
||||
echo "Generating magic bitboards"
|
||||
let start = cpuTime()
|
||||
let it {.used.} = computeMagics()
|
||||
let tot = round(cpuTime() - start, 3)
|
||||
|
||||
echo &"Generated magic bitboards in {tot} seconds with {it} iterations"
|
||||
var
|
||||
rookTableSize = 0
|
||||
rookTableCount = 0
|
||||
bishopTableSize = 0
|
||||
bishopTableCount = 0
|
||||
for i in 0..63:
|
||||
inc(rookTableCount, len(ROOK_MOVES[i]))
|
||||
inc(bishopTableCount, len(BISHOP_MOVES[i]))
|
||||
inc(rookTableSize, len(ROOK_MOVES[i]) * sizeof(Bitboard) + sizeof(seq[Bitboard]))
|
||||
inc(bishopTableSize, len(BISHOP_MOVES[i]) * sizeof(Bitboard) + sizeof(seq[Bitboard]))
|
||||
|
||||
echo &"There are {rookTableCount} entries in the move table for rooks (total size: ~{round(rookTableSize / 1024, 3)} KiB)"
|
||||
echo &"There are {bishopTableCount} entries in the move table for bishops (total size: ~{round(bishopTableSize / 1024, 3)} KiB)"
|
||||
var magics = newTable[string, array[64, MagicEntry]]()
|
||||
var moves = newTable[string, array[64, seq[Bitboard]]]()
|
||||
magics["rooks"] = ROOK_MAGICS
|
||||
magics["bishops"] = BISHOP_MAGICS
|
||||
moves["rooks"] = ROOK_MOVES
|
||||
moves["bishops"] = BISHOP_MOVES
|
||||
let
|
||||
magicsJson = magics.toJSON()
|
||||
movesJson = moves.toJSON()
|
||||
var currentFile = currentSourcePath()
|
||||
var path = joinPath(currentFile.parentDir(), "resources")
|
||||
writeFile(joinPath(path, "magics.json"), magicsJson)
|
||||
writeFile(joinPath(path, "movesets.json"), movesJson)
|
||||
echo &"Dumped data to disk (approx. {round(((len(movesJson) + len(magicsJson)) / 1024) / 1024, 2)} MiB)"
|
||||
else:
|
||||
func buildPath: string {.compileTime.} =
|
||||
result = currentSourcePath()
|
||||
result = joinPath(result.parentDir(), "resources")
|
||||
|
||||
const path = buildPath()
|
||||
echo "Loading magic bitboards"
|
||||
const
|
||||
magicFile = staticRead(joinPath(path, "magics.json"))
|
||||
movesFile = staticRead(joinPath(path, "movesets.json"))
|
||||
var magics = magicFile.fromJson(TableRef[string, array[64, MagicEntry]])
|
||||
var moves = movesFile.fromJson(TableRef[string, array[64, seq[Bitboard]]])
|
||||
ROOK_MAGICS = magics["rooks"]
|
||||
BISHOP_MAGICS = magics["bishops"]
|
||||
ROOK_MOVES = moves["rooks"]
|
||||
BISHOP_MOVES = moves["bishops"]
|
|
@ -0,0 +1,124 @@
|
|||
# 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
|
||||
|
||||
|
||||
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()})"
|
||||
|
||||
|
||||
proc basicTests* =
|
||||
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)
|
|
@ -0,0 +1,443 @@
|
|||
# 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.
|
||||
|
||||
## Move generation logic
|
||||
|
||||
when not defined(danger):
|
||||
import std/strformat
|
||||
|
||||
|
||||
import bitboards
|
||||
import board
|
||||
import magics
|
||||
import pieces
|
||||
import moves
|
||||
import position
|
||||
import rays
|
||||
import misc
|
||||
|
||||
|
||||
export bitboards, magics, pieces, moves, position, rays, misc, board
|
||||
|
||||
|
||||
|
||||
proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
|
||||
let
|
||||
sideToMove = self.position.sideToMove
|
||||
nonSideToMove = sideToMove.opposite()
|
||||
pawns = self.getBitboard(Pawn, sideToMove)
|
||||
occupancy = self.getOccupancy()
|
||||
# We can only capture enemy pieces (except the king)
|
||||
enemyPieces = self.getOccupancyFor(nonSideToMove)
|
||||
epTarget = self.position.enPassantSquare
|
||||
diagonalPins = self.position.diagonalPins
|
||||
orthogonalPins = self.position.orthogonalPins
|
||||
promotionRank = if sideToMove == White: getRankMask(0) else: getRankMask(7)
|
||||
# The rank where each color's side starts
|
||||
# TODO: Give names to ranks and files so we don't have to assume a
|
||||
# specific board layout when calling get(Rank|File)Mask
|
||||
startingRank = if sideToMove == White: getRankMask(6) else: getRankMask(1)
|
||||
friendlyKing = self.getBitboard(King, sideToMove).toSquare()
|
||||
|
||||
# Single and double pushes
|
||||
|
||||
# If a pawn is pinned diagonally, it cannot push forward
|
||||
let
|
||||
# If a pawn is pinned horizontally, it cannot move either. It can move vertically
|
||||
# though. Thanks to Twipply for the tip on how to get a horizontal pin mask out of
|
||||
# our orthogonal bitboard :)
|
||||
horizontalPins = Bitboard((0xFF'u64 shl (rankFromSquare(friendlyKing).uint64 * 8))) and orthogonalPins
|
||||
pushablePawns = pawns and not diagonalPins and not horizontalPins
|
||||
singlePushes = (pushablePawns.forwardRelativeTo(sideToMove) and not occupancy) and destinationMask
|
||||
# We do this weird dance instead of using doubleForwardRelativeTo() because that doesn't have any
|
||||
# way to check if there's pieces on the two squares ahead of the pawn
|
||||
var canDoublePush = pushablePawns and startingRank
|
||||
canDoublePush = canDoublePush.forwardRelativeTo(sideToMove) and not occupancy
|
||||
canDoublePush = canDoublePush.forwardRelativeTo(sideToMove) and not occupancy and destinationMask
|
||||
|
||||
for pawn in singlePushes:
|
||||
let pawnBB = pawn.toBitboard()
|
||||
if promotionRank.contains(pawn):
|
||||
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]:
|
||||
moves.add(createMove(pawnBB.backwardRelativeTo(sideToMove), pawn, promotion))
|
||||
else:
|
||||
moves.add(createMove(pawnBB.backwardRelativeTo(sideToMove), pawn))
|
||||
|
||||
for pawn in canDoublePush:
|
||||
moves.add(createMove(pawn.toBitboard().doubleBackwardRelativeTo(sideToMove), pawn, DoublePush))
|
||||
|
||||
let
|
||||
canCapture = pawns and not orthogonalPins
|
||||
canCaptureLeftUnpinned = (canCapture and not diagonalPins).forwardLeftRelativeTo(sideToMove) and enemyPieces and destinationMask
|
||||
canCaptureRightUnpinned = (canCapture and not diagonalPins).forwardRightRelativeTo(sideToMove) and enemyPieces and destinationMask
|
||||
|
||||
for pawn in canCaptureRightUnpinned:
|
||||
let pawnBB = pawn.toBitboard()
|
||||
if promotionRank.contains(pawn):
|
||||
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]:
|
||||
moves.add(createMove(pawnBB.backwardLeftRelativeTo(sideToMove), pawn, Capture, promotion))
|
||||
else:
|
||||
moves.add(createMove(pawnBB.backwardLeftRelativeTo(sideToMove), pawn, Capture))
|
||||
|
||||
for pawn in canCaptureLeftUnpinned:
|
||||
let pawnBB = pawn.toBitboard()
|
||||
if promotionRank.contains(pawn):
|
||||
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]:
|
||||
moves.add(createMove(pawnBB.backwardRightRelativeTo(sideToMove), pawn, Capture, promotion))
|
||||
else:
|
||||
moves.add(createMove(pawnBB.backwardRightRelativeTo(sideToMove), pawn, Capture))
|
||||
|
||||
# Special cases for pawns pinned diagonally that can capture their pinners
|
||||
|
||||
let
|
||||
canCaptureLeft = canCapture.forwardLeftRelativeTo(sideToMove) and enemyPieces and destinationMask
|
||||
canCaptureRight = canCapture.forwardRightRelativeTo(sideToMove) and enemyPieces and destinationMask
|
||||
leftPinnedCanCapture = (canCaptureLeft and diagonalPins) and not canCaptureLeftUnpinned
|
||||
rightPinnedCanCapture = ((canCaptureRight and diagonalPins) and not canCaptureRightUnpinned) and not canCaptureRightUnpinned
|
||||
|
||||
for pawn in leftPinnedCanCapture:
|
||||
let pawnBB = pawn.toBitboard()
|
||||
if promotionRank.contains(pawn):
|
||||
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]:
|
||||
moves.add(createMove(pawnBB.backwardRightRelativeTo(sideToMove), pawn, Capture, promotion))
|
||||
else:
|
||||
moves.add(createMove(pawnBB.backwardRightRelativeTo(sideToMove), pawn, Capture))
|
||||
|
||||
for pawn in rightPinnedCanCapture:
|
||||
let pawnBB = pawn.toBitboard()
|
||||
if promotionRank.contains(pawn):
|
||||
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]:
|
||||
moves.add(createMove(pawnBB.backwardLeftRelativeTo(sideToMove), pawn, Capture, promotion))
|
||||
else:
|
||||
moves.add(createMove(pawnBB.backwardLeftRelativeTo(sideToMove), pawn, Capture))
|
||||
|
||||
# En passant captures
|
||||
var epBitboard = if epTarget != nullSquare(): epTarget.toBitboard() else: Bitboard(0)
|
||||
if epBitboard != 0:
|
||||
# See if en passant would create a check
|
||||
let
|
||||
epPawn = epBitboard.backwardRelativeTo(sideToMove)
|
||||
epLeft = pawns.forwardLeftRelativeTo(sideToMove) and epBitboard and destinationMask
|
||||
epRight = pawns.forwardRightRelativeTo(sideToMove) and epBitboard and destinationMask
|
||||
# Note: it's possible for two pawns to both have rights to do an en passant! See
|
||||
# 4k3/8/8/2PpP3/8/8/8/4K3 w - d6 0 1
|
||||
if epLeft != 0:
|
||||
# We basically simulate the en passant and see if the resulting
|
||||
# occupancy bitboard has the king in check
|
||||
let
|
||||
friendlyPawn = epBitboard.backwardRightRelativeTo(sideToMove)
|
||||
newOccupancy = occupancy and not epPawn and not friendlyPawn or epBitboard
|
||||
# We also need to temporarily remove the en passant pawn from
|
||||
# our bitboards, or else functions like getPawnAttacks won't
|
||||
# get the news that the pawn is gone and will still think the
|
||||
# king is in check after en passant when it actually isn't
|
||||
# (see pos fen rnbqkbnr/pppp1ppp/8/2P5/K7/8/PPPP1PPP/RNBQ1BNR b kq - 0 1 moves b7b5 c5b6)
|
||||
let epPawnSquare = epPawn.toSquare()
|
||||
let epPiece = self.getPiece(epPawnSquare)
|
||||
self.removePiece(epPawnSquare)
|
||||
if not self.isOccupancyAttacked(friendlyKing, newOccupancy):
|
||||
# En passant does not create a check on the king: all good
|
||||
moves.add(createMove(friendlyPawn, epBitboard, EnPassant))
|
||||
self.spawnPiece(epPawnSquare, epPiece)
|
||||
if epRight != 0:
|
||||
# Note that this isn't going to be the same pawn from the previous if block!
|
||||
let
|
||||
friendlyPawn = epBitboard.backwardLeftRelativeTo(sideToMove)
|
||||
newOccupancy = occupancy and not epPawn and not friendlyPawn or epBitboard
|
||||
let epPawnSquare = epPawn.toSquare()
|
||||
let epPiece = self.getPiece(epPawnSquare)
|
||||
self.removePiece(epPawnSquare)
|
||||
if not self.isOccupancyAttacked(friendlyKing, newOccupancy):
|
||||
# En passant does not create a check on the king: all good
|
||||
moves.add(createMove(friendlyPawn, epBitboard, EnPassant))
|
||||
self.spawnPiece(epPawnSquare, epPiece)
|
||||
|
||||
|
||||
proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
|
||||
let
|
||||
sideToMove = self.position.sideToMove
|
||||
occupancy = self.getOccupancy()
|
||||
enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, sideToMove.opposite())
|
||||
rooks = self.getBitboard(Rook, sideToMove)
|
||||
queens = self.getBitboard(Queen, sideToMove)
|
||||
movableRooks = not self.position.diagonalPins and (queens or rooks)
|
||||
pinMask = self.position.orthogonalPins
|
||||
pinnedRooks = movableRooks and pinMask
|
||||
unpinnedRooks = movableRooks and not pinnedRooks
|
||||
for square in pinnedRooks:
|
||||
let
|
||||
blockers = occupancy and Rook.getRelevantBlockers(square)
|
||||
moveset = getRookMoves(square, blockers)
|
||||
for target in moveset and pinMask and destinationMask and not enemyPieces:
|
||||
moves.add(createMove(square, target))
|
||||
for target in moveset and enemyPieces and pinMask and destinationMask:
|
||||
moves.add(createMove(square, target, Capture))
|
||||
for square in unpinnedRooks:
|
||||
let
|
||||
blockers = occupancy and Rook.getRelevantBlockers(square)
|
||||
moveset = getRookMoves(square, blockers)
|
||||
for target in moveset and destinationMask and not enemyPieces:
|
||||
moves.add(createMove(square, target))
|
||||
for target in moveset and enemyPieces and destinationMask:
|
||||
moves.add(createMove(square, target, Capture))
|
||||
|
||||
|
||||
proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
|
||||
let
|
||||
sideToMove = self.position.sideToMove
|
||||
occupancy = self.getOccupancy()
|
||||
enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, sideToMove.opposite())
|
||||
bishops = self.getBitboard(Bishop, sideToMove)
|
||||
queens = self.getBitboard(Queen, sideToMove)
|
||||
movableBishops = not self.position.orthogonalPins and (queens or bishops)
|
||||
pinMask = self.position.diagonalPins
|
||||
pinnedBishops = movableBishops and pinMask
|
||||
unpinnedBishops = movableBishops and not pinnedBishops
|
||||
for square in pinnedBishops:
|
||||
let
|
||||
blockers = occupancy and Bishop.getRelevantBlockers(square)
|
||||
moveset = getBishopMoves(square, blockers)
|
||||
for target in moveset and pinMask and destinationMask and not enemyPieces:
|
||||
moves.add(createMove(square, target))
|
||||
for target in moveset and pinMask and enemyPieces and destinationMask:
|
||||
moves.add(createMove(square, target, Capture))
|
||||
for square in unpinnedBishops:
|
||||
let
|
||||
blockers = occupancy and Bishop.getRelevantBlockers(square)
|
||||
moveset = getBishopMoves(square, blockers)
|
||||
for target in moveset and destinationMask and not enemyPieces:
|
||||
moves.add(createMove(square, target))
|
||||
for target in moveset and enemyPieces and destinationMask:
|
||||
moves.add(createMove(square, target, Capture))
|
||||
|
||||
|
||||
proc generateKingMoves(self: Chessboard, moves: var MoveList) =
|
||||
let
|
||||
sideToMove = self.position.sideToMove
|
||||
king = self.getBitboard(King, sideToMove)
|
||||
occupancy = self.getOccupancy()
|
||||
nonSideToMove = sideToMove.opposite()
|
||||
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
|
||||
bitboard = getKingAttacks(king.toSquare())
|
||||
noKingOccupancy = occupancy and not king
|
||||
for square in bitboard and not occupancy:
|
||||
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
||||
moves.add(createMove(king, square))
|
||||
for square in bitboard and enemyPieces:
|
||||
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
||||
moves.add(createMove(king, square, Capture))
|
||||
|
||||
|
||||
proc generateKnightMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
|
||||
let
|
||||
sideToMove = self.position.sideToMove
|
||||
knights = self.getBitboard(Knight, sideToMove)
|
||||
nonSideToMove = sideToMove.opposite()
|
||||
pinned = self.position.diagonalPins or self.position.orthogonalPins
|
||||
unpinnedKnights = knights and not pinned
|
||||
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
|
||||
for square in unpinnedKnights:
|
||||
let bitboard = getKnightAttacks(square)
|
||||
for target in bitboard and destinationMask and not enemyPieces:
|
||||
moves.add(createMove(square, target))
|
||||
for target in bitboard and destinationMask and enemyPieces:
|
||||
moves.add(createMove(square, target, Capture))
|
||||
|
||||
|
||||
proc generateCastling(self: Chessboard, moves: var MoveList) =
|
||||
let
|
||||
sideToMove = self.position.sideToMove
|
||||
castlingRights = self.canCastle()
|
||||
kingSquare = self.getBitboard(King, sideToMove).toSquare()
|
||||
kingPiece = self.getPiece(kingSquare)
|
||||
if castlingRights.king:
|
||||
moves.add(createMove(kingSquare, kingPiece.kingSideCastling(), Castle))
|
||||
if castlingRights.queen:
|
||||
moves.add(createMove(kingSquare, kingPiece.queenSideCastling(), Castle))
|
||||
|
||||
|
||||
proc generateMoves*(self: Chessboard, moves: var MoveList) =
|
||||
## Generates the list of all possible legal moves
|
||||
## in the current position
|
||||
if self.position.halfMoveClock >= 100:
|
||||
# Draw by 50-move rule
|
||||
return
|
||||
let sideToMove = self.position.sideToMove
|
||||
# TODO: Check for draw by insufficient material
|
||||
# TODO: Check for repetitions (requires zobrist hashing + table)
|
||||
self.generateKingMoves(moves)
|
||||
if self.position.checkers.countSquares() > 1:
|
||||
# King is in double check: no need to generate any more
|
||||
# moves
|
||||
return
|
||||
|
||||
self.generateCastling(moves)
|
||||
|
||||
# We pass a mask to our move generators to remove stuff
|
||||
# like our friendly pieces from the set of possible
|
||||
# target squares, as well as to ensure checks are not
|
||||
# ignored
|
||||
|
||||
var destinationMask: Bitboard
|
||||
if not self.inCheck():
|
||||
# Not in check: cannot move over friendly pieces
|
||||
destinationMask = not self.getOccupancyFor(sideToMove)
|
||||
else:
|
||||
# We *are* in check (from a single piece, because the two checks
|
||||
# case was handled above already). If the piece is a slider, we'll
|
||||
# extract the ray from it to our king and add the checking piece to
|
||||
# it, meaning the only legal moves are those that either block the
|
||||
# check or capture the checking piece. For other non-sliding pieces
|
||||
# the ray will be empty so the only legal move will be to capture
|
||||
# the checking piece (or moving the king)
|
||||
let
|
||||
checker = self.position.checkers.lowestSquare()
|
||||
checkerBB = checker.toBitboard()
|
||||
epTarget = self.position.enPassantSquare
|
||||
checkerPiece = self.getPiece(checker)
|
||||
destinationMask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checkerBB
|
||||
if checkerPiece.kind == Pawn and checkerBB.backwardRelativeTo(checkerPiece.color).toSquare() == epTarget:
|
||||
# We are in check by a pawn that pushed two squares: add the ep target square to the set of
|
||||
# squares that our friendly pieces can move to in order to resolve it. This will do nothing
|
||||
# for most pieces, because the move generators won't allow them to move there, but it does matter
|
||||
# for pawns
|
||||
destinationMask = destinationMask or epTarget.toBitboard()
|
||||
self.generatePawnMoves(moves, destinationMask)
|
||||
self.generateKnightMoves(moves, destinationMask)
|
||||
self.generateRookMoves(moves, destinationMask)
|
||||
self.generateBishopMoves(moves, destinationMask)
|
||||
# Queens are just handled rooks + bishops
|
||||
|
||||
|
||||
proc doMove*(self: Chessboard, move: Move) =
|
||||
## Internal function called by makeMove after
|
||||
## performing legality checks. Can be used in
|
||||
## performance-critical paths where a move is
|
||||
## already known to be legal (i.e. during search)
|
||||
|
||||
# Record final position for future reference
|
||||
self.positions.add(self.position)
|
||||
|
||||
# Final checks
|
||||
let piece = self.getPiece(move.startSquare)
|
||||
when not defined(danger):
|
||||
doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}"
|
||||
|
||||
var
|
||||
halfMoveClock = self.position.halfMoveClock
|
||||
fullMoveCount = self.position.fullMoveCount
|
||||
enPassantTarget = nullSquare()
|
||||
|
||||
# Needed to detect draw by the 50 move rule
|
||||
if piece.kind == Pawn or move.isCapture() or move.isEnPassant():
|
||||
# Number of half-moves since the last reversible half-move
|
||||
halfMoveClock = 0
|
||||
else:
|
||||
inc(halfMoveClock)
|
||||
if piece.color == Black:
|
||||
inc(fullMoveCount)
|
||||
|
||||
if move.isDoublePush():
|
||||
enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()
|
||||
|
||||
# Create new position
|
||||
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
|
||||
halfMoveClock: halfMoveClock,
|
||||
fullMoveCount: fullMoveCount,
|
||||
sideToMove: self.position.sideToMove.opposite(),
|
||||
enPassantSquare: enPassantTarget,
|
||||
pieces: self.position.pieces,
|
||||
castlingAvailability: self.position.castlingAvailability
|
||||
)
|
||||
# Update position metadata
|
||||
|
||||
if move.isEnPassant():
|
||||
# Make the en passant pawn disappear
|
||||
self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare())
|
||||
|
||||
if move.isCastling() or piece.kind == King:
|
||||
# If the king has moved, all castling rights for the side to
|
||||
# move are revoked
|
||||
self.position.castlingAvailability[piece.color.int] = (false, false)
|
||||
if move.isCastling():
|
||||
# Move the rook where it belongs
|
||||
if move.targetSquare == piece.kingSideCastling():
|
||||
let rook = self.getPiece(piece.color.kingSideRook())
|
||||
self.movePiece(piece.color.kingSideRook(), rook.kingSideCastling())
|
||||
if move.targetSquare == piece.queenSideCastling():
|
||||
let rook = self.getPiece(piece.color.queenSideRook())
|
||||
self.movePiece(piece.color.queenSideRook(), rook.queenSideCastling())
|
||||
|
||||
if piece.kind == Rook:
|
||||
# If a rook on either side moves, castling rights are permanently revoked
|
||||
# on that side
|
||||
if move.startSquare == piece.color.kingSideRook():
|
||||
self.position.castlingAvailability[piece.color.int].king = false
|
||||
elif move.startSquare == piece.color.queenSideRook():
|
||||
self.position.castlingAvailability[piece.color.int].queen = false
|
||||
|
||||
if move.isCapture():
|
||||
# Get rid of captured pieces
|
||||
let captured = self.getPiece(move.targetSquare)
|
||||
self.removePiece(move.targetSquare)
|
||||
# If a rook has been captured, castling on that side is prohibited
|
||||
if captured.kind == Rook:
|
||||
if move.targetSquare == captured.color.kingSideRook():
|
||||
self.position.castlingAvailability[captured.color.int].king = false
|
||||
elif move.targetSquare == captured.color.queenSideRook():
|
||||
self.position.castlingAvailability[captured.color.int].queen = false
|
||||
|
||||
# Move the piece to its target square
|
||||
self.movePiece(move)
|
||||
if move.isPromotion():
|
||||
# Move is a pawn promotion: get rid of the pawn
|
||||
# and spawn a new piece
|
||||
self.removePiece(move.targetSquare)
|
||||
case move.getPromotionType():
|
||||
of PromoteToBishop:
|
||||
self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color))
|
||||
of PromoteToKnight:
|
||||
self.spawnPiece(move.targetSquare, Piece(kind: Knight, color: piece.color))
|
||||
of PromoteToRook:
|
||||
self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: piece.color))
|
||||
of PromoteToQueen:
|
||||
self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color))
|
||||
else:
|
||||
# Unreachable
|
||||
discard
|
||||
# Updates checks and pins for the (new) side to move
|
||||
self.updateChecksAndPins()
|
||||
|
||||
|
||||
proc isLegal*(self: Chessboard, move: Move): bool {.inline.} =
|
||||
## Returns whether the given move is legal
|
||||
var moves = MoveList()
|
||||
self.generateMoves(moves)
|
||||
return move in moves
|
||||
|
||||
|
||||
proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} =
|
||||
## Makes a move on the board
|
||||
result = move
|
||||
# Updates checks and pins for the side to move
|
||||
if not self.isLegal(move):
|
||||
return nullMove()
|
||||
self.doMove(move)
|
||||
|
||||
|
||||
proc unmakeMove*(self: Chessboard) =
|
||||
## Reverts to the previous board position,
|
||||
## if one exists
|
||||
self.position = self.positions.pop()
|
||||
self.update()
|
|
@ -0,0 +1,161 @@
|
|||
# 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.
|
||||
|
||||
## Handling of moves
|
||||
import pieces
|
||||
|
||||
|
||||
type
|
||||
MoveFlag* = enum
|
||||
## An enumeration of move flags
|
||||
Default = 0'u16, # No flag
|
||||
EnPassant = 1, # Move is a capture with en passant
|
||||
Capture = 2, # Move is a capture
|
||||
DoublePush = 4, # Move is a double pawn push
|
||||
# Castling metadata
|
||||
Castle = 8,
|
||||
# Pawn promotion metadata
|
||||
PromoteToQueen = 16,
|
||||
PromoteToRook = 32,
|
||||
PromoteToBishop = 64,
|
||||
PromoteToKnight = 128
|
||||
|
||||
Move* = object
|
||||
## A chess move
|
||||
startSquare*: Square
|
||||
targetSquare*: Square
|
||||
flags*: uint16
|
||||
|
||||
MoveList* = object
|
||||
## A list of moves
|
||||
data: array[218, Move]
|
||||
len: int8
|
||||
|
||||
func `[]`*(self: MoveList, i: SomeInteger): Move =
|
||||
when not defined(danger):
|
||||
if i >= self.len:
|
||||
raise newException(IndexDefect, &"move list access out of bounds ({i} >= {self.len})")
|
||||
result = self.data[i]
|
||||
|
||||
iterator items*(self: MoveList): Move =
|
||||
var i = 0
|
||||
while self.len > i:
|
||||
yield self.data[i]
|
||||
inc(i)
|
||||
|
||||
|
||||
iterator pairs*(self: MoveList): tuple[i: int, move: Move] =
|
||||
var i = 0
|
||||
for item in self:
|
||||
yield (i, item)
|
||||
|
||||
|
||||
func `$`*(self: MoveList): string =
|
||||
result &= "["
|
||||
for i, move in self:
|
||||
result &= $move
|
||||
if i < self.len:
|
||||
result &= ", "
|
||||
result &= "]"
|
||||
|
||||
|
||||
func add*(self: var MoveList, move: Move) {.inline.} =
|
||||
self.data[self.len] = move
|
||||
inc(self.len)
|
||||
|
||||
|
||||
func clear*(self: var MoveList) {.inline.} =
|
||||
self.len = 0
|
||||
|
||||
|
||||
func contains*(self: MoveList, move: Move): bool {.inline.} =
|
||||
for item in self:
|
||||
if move == item:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func len*(self: MoveList): int {.inline.} = self.len
|
||||
|
||||
|
||||
# A bunch of move creation utilities
|
||||
|
||||
func createMove*(startSquare, targetSquare: Square, flags: varargs[MoveFlag]): Move =
|
||||
result = Move(startSquare: startSquare, targetSquare: targetSquare, flags: Default.uint16)
|
||||
for flag in flags:
|
||||
result.flags = result.flags or flag.uint16
|
||||
|
||||
proc createMove*(startSquare, targetSquare: string, flags: varargs[MoveFlag]): Move =
|
||||
result = createMove(startSquare.toSquare(), targetSquare.toSquare(), flags)
|
||||
|
||||
func createMove*(startSquare, targetSquare: SomeInteger, flags: varargs[MoveFlag]): Move =
|
||||
result = createMove(Square(startSquare.int8), Square(targetSquare.int8), flags)
|
||||
|
||||
|
||||
func createMove*(startSquare: Square, targetSquare: SomeInteger, flags: varargs[MoveFlag]): Move =
|
||||
result = createMove(startSquare, Square(targetSquare.int8), flags)
|
||||
|
||||
|
||||
func nullMove*: Move {.inline.} = createMove(nullSquare(), nullSquare())
|
||||
|
||||
func isPromotion*(move: Move): bool {.inline.} =
|
||||
## Returns whether the given move is a
|
||||
## pawn promotion
|
||||
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]:
|
||||
if (move.flags and promotion.uint16) != 0:
|
||||
return true
|
||||
|
||||
|
||||
func getPromotionType*(move: Move): MoveFlag {.inline.} =
|
||||
## Returns the promotion type of the given move.
|
||||
## The return value of this function is only valid
|
||||
## if isPromotion() returns true
|
||||
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]:
|
||||
if (move.flags and promotion.uint16) != 0:
|
||||
return promotion
|
||||
|
||||
|
||||
func isCapture*(move: Move): bool {.inline.} =
|
||||
## Returns whether the given move is a
|
||||
## cature
|
||||
result = (move.flags and Capture.uint16) != 0
|
||||
|
||||
|
||||
func isCastling*(move: Move): bool {.inline.} =
|
||||
## Returns whether the given move is a
|
||||
## castling move
|
||||
result = (move.flags and Castle.uint16) != 0
|
||||
|
||||
|
||||
func isEnPassant*(move: Move): bool {.inline.} =
|
||||
## Returns whether the given move is an
|
||||
## en passant capture
|
||||
result = (move.flags and EnPassant.uint16) != 0
|
||||
|
||||
|
||||
func isDoublePush*(move: Move): bool {.inline.} =
|
||||
## Returns whether the given move is a
|
||||
## double pawn push
|
||||
result = (move.flags and DoublePush.uint16) != 0
|
||||
|
||||
|
||||
func getFlags*(move: Move): seq[MoveFlag] =
|
||||
## Gets all the flags of this move
|
||||
for flag in [EnPassant, Capture, DoublePush, Castle,
|
||||
PromoteToBishop, PromoteToKnight, PromoteToQueen,
|
||||
PromoteToRook]:
|
||||
if (move.flags and flag.uint16) == flag.uint16:
|
||||
result.add(flag)
|
||||
if result.len() == 0:
|
||||
result.add(Default)
|
|
@ -0,0 +1,221 @@
|
|||
# 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 handling of squares, board indeces and pieces
|
||||
import std/strutils
|
||||
import std/strformat
|
||||
|
||||
|
||||
type
|
||||
Square* = distinct int8
|
||||
## A square
|
||||
|
||||
PieceColor* = enum
|
||||
## A piece color enumeration
|
||||
White = 0'i8
|
||||
Black = 1
|
||||
None
|
||||
|
||||
PieceKind* = enum
|
||||
## A chess piece enumeration
|
||||
Bishop = 0'i8
|
||||
King = 1
|
||||
Knight = 2
|
||||
Pawn = 3
|
||||
Queen = 4
|
||||
Rook = 5
|
||||
Empty = 6 # No piece
|
||||
|
||||
|
||||
Piece* = object
|
||||
## A chess piece
|
||||
color*: PieceColor
|
||||
kind*: PieceKind
|
||||
|
||||
|
||||
func nullPiece*: Piece {.inline.} = Piece(kind: Empty, color: None)
|
||||
func nullSquare*: Square {.inline.} = Square(-1'i8)
|
||||
func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White)
|
||||
func isValid*(a: Square): bool {.inline.} = a.int8 in 0..63
|
||||
func isLightSquare*(a: Square): bool {.inline.} = (a.int8 and 2) == 0
|
||||
func `==`*(a, b: Square): bool {.inline.} = a.int8 == b.int8
|
||||
func `!=`*(a, b: Square): bool {.inline.} = a.int8 != b.int8
|
||||
func `-`*(a, b: Square): Square {.inline.} = Square(a.int8 - b.int8)
|
||||
func `-`*(a: Square, b: SomeInteger): Square {.inline.} = Square(a.int8 - b.int8)
|
||||
func `-`*(a: SomeInteger, b: Square): Square {.inline.} = Square(a.int8 - b.int8)
|
||||
func `+`*(a, b: Square): Square {.inline.} = Square(a.int8 + b.int8)
|
||||
func `+`*(a: Square, b: SomeInteger): Square {.inline.} = Square(a.int8 + b.int8)
|
||||
func `+`*(a: SomeInteger, b: Square): Square {.inline.} = Square(a.int8 + b.int8)
|
||||
|
||||
func fileFromSquare*(square: Square): int8 = square.int8 mod 8
|
||||
func rankFromSquare*(square: Square): int8 = square.int8 div 8
|
||||
|
||||
|
||||
func makeSquare*(rank, file: SomeInteger): Square = Square((rank * 8) + file)
|
||||
|
||||
|
||||
proc toSquare*(s: string): Square {.discardable.} =
|
||||
## Converts a square square from algebraic
|
||||
## notation to its corresponding row and column
|
||||
## in the chess grid (0 indexed)
|
||||
if len(s) != 2:
|
||||
raise newException(ValueError, "algebraic position must be of length 2")
|
||||
|
||||
var s = s.toLowerAscii()
|
||||
if s[0] notin 'a'..'h':
|
||||
raise newException(ValueError, &"algebraic position has invalid first character ('{s[0]}')")
|
||||
if s[1] notin '1'..'8':
|
||||
raise newException(ValueError, &"algebraic position has invalid second character ('{s[1]}')")
|
||||
|
||||
return Square((s[0].uint8 - uint8('a')) + ((s[1].uint8 - uint8('1')) xor 7) * 8)
|
||||
|
||||
|
||||
proc toAlgebraic*(square: Square): string {.inline.} =
|
||||
## Converts a square from our internal rank/file
|
||||
## notation to a square in algebraic notation
|
||||
let
|
||||
file = char('a'.uint8 + (square.uint64 and 7))
|
||||
rank = char('1'.uint8 + ((square.uint64 div 8) xor 7))
|
||||
return &"{file}{rank}"
|
||||
|
||||
|
||||
proc `$`*(square: Square): string = square.toAlgebraic()
|
||||
|
||||
func kingSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare())
|
||||
func queenSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "a1".toSquare() else: "a8".toSquare())
|
||||
|
||||
func kingSideCastling*(piece: Piece): Square {.inline.} =
|
||||
case piece.kind:
|
||||
of Rook:
|
||||
case piece.color:
|
||||
of White:
|
||||
return "f1".toSquare()
|
||||
of Black:
|
||||
return "f8".toSquare()
|
||||
else:
|
||||
discard
|
||||
of King:
|
||||
case piece.color:
|
||||
of White:
|
||||
return "g1".toSquare()
|
||||
of Black:
|
||||
return "g8".toSquare()
|
||||
else:
|
||||
discard
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
func queenSideCastling*(piece: Piece): Square {.inline.} =
|
||||
case piece.kind:
|
||||
of Rook:
|
||||
case piece.color:
|
||||
of White:
|
||||
return "d1".toSquare()
|
||||
of Black:
|
||||
return "d8".toSquare()
|
||||
else:
|
||||
discard
|
||||
of King:
|
||||
case piece.color:
|
||||
of White:
|
||||
return "c1".toSquare()
|
||||
of Black:
|
||||
return "c8".toSquare()
|
||||
else:
|
||||
discard
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
proc toPretty*(piece: Piece): string =
|
||||
case piece.color:
|
||||
of White:
|
||||
case piece.kind:
|
||||
of King:
|
||||
return "\U2654"
|
||||
of Queen:
|
||||
return "\U2655"
|
||||
of Rook:
|
||||
return "\U2656"
|
||||
of Bishop:
|
||||
return "\U2657"
|
||||
of Knight:
|
||||
return "\U2658"
|
||||
of Pawn:
|
||||
return "\U2659"
|
||||
else:
|
||||
discard
|
||||
of Black:
|
||||
case piece.kind:
|
||||
of King:
|
||||
return "\U265A"
|
||||
of Queen:
|
||||
return "\U265B"
|
||||
of Rook:
|
||||
return "\U265C"
|
||||
of Bishop:
|
||||
return "\U265D"
|
||||
of Knight:
|
||||
return "\U265E"
|
||||
of Pawn:
|
||||
return "\240\159\168\133"
|
||||
else:
|
||||
discard
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
func toChar*(piece: Piece): char =
|
||||
case piece.kind:
|
||||
of Bishop:
|
||||
result = 'b'
|
||||
of King:
|
||||
result = 'k'
|
||||
of Knight:
|
||||
result = 'n'
|
||||
of Pawn:
|
||||
result = 'p'
|
||||
of Queen:
|
||||
result = 'q'
|
||||
of Rook:
|
||||
result = 'r'
|
||||
else:
|
||||
discard
|
||||
if piece.color == White:
|
||||
result = result.toUpperAscii()
|
||||
|
||||
|
||||
func fromChar*(c: char): Piece =
|
||||
var
|
||||
kind: PieceKind
|
||||
color = Black
|
||||
case c.toLowerAscii():
|
||||
of 'b':
|
||||
kind = Bishop
|
||||
of 'k':
|
||||
kind = King
|
||||
of 'n':
|
||||
kind = Knight
|
||||
of 'p':
|
||||
kind = Pawn
|
||||
of 'q':
|
||||
kind = Queen
|
||||
of 'r':
|
||||
kind = Rook
|
||||
else:
|
||||
discard
|
||||
if c.isUpperAscii():
|
||||
color = White
|
||||
result = Piece(kind: kind, color: color)
|
|
@ -0,0 +1,79 @@
|
|||
# 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 bitboards
|
||||
import magics
|
||||
import pieces
|
||||
|
||||
|
||||
|
||||
type
|
||||
|
||||
Position* = object
|
||||
## A chess position
|
||||
|
||||
# Castling availability. This just keeps track
|
||||
# of whether the king or the rooks on either side
|
||||
# moved, the actual checks for the legality of castling
|
||||
# are done elsewhere
|
||||
castlingAvailability*: array[2, tuple[queen, king: bool]]
|
||||
# Number of half-moves that were performed
|
||||
# to reach this position starting from the
|
||||
# root of the tree
|
||||
plyFromRoot*: int8
|
||||
# Number of half moves since
|
||||
# last piece capture or pawn movement.
|
||||
# Used for the 50-move rule
|
||||
halfMoveClock*: int8
|
||||
# Full move counter. Increments
|
||||
# every 2 ply (half-moves)
|
||||
fullMoveCount*: int8
|
||||
# En passant target square (see https://en.wikipedia.org/wiki/En_passant)
|
||||
enPassantSquare*: Square
|
||||
|
||||
# The side to move
|
||||
sideToMove*: PieceColor
|
||||
# Positional bitboards for all pieces
|
||||
pieces*: array[2, array[6, Bitboard]]
|
||||
# Pieces pinned for the current side to move
|
||||
diagonalPins*: Bitboard # Pinned diagonally (by a queen or bishop)
|
||||
orthogonalPins*: Bitboard # Pinned orthogonally (by a queen or rook)
|
||||
# Pieces checking the current side to move
|
||||
checkers*: Bitboard
|
||||
|
||||
|
||||
func getKingStartingSquare*(color: PieceColor): Square {.inline.} =
|
||||
## Retrieves the starting square of the king
|
||||
## for the given color
|
||||
case color:
|
||||
of White:
|
||||
return "e1".toSquare()
|
||||
of Black:
|
||||
return "e8".toSquare()
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
func `[]`*(self: array[2, array[6, Bitboard]], color: PieceColor): ptr array[6, Bitboard] {.inline.} = addr self[color.int]
|
||||
func `[]`*(self: array[6, Bitboard], kind: PieceKind): ptr Bitboard {.inline.} = addr self[kind.int]
|
||||
func `[]=`*(self: var array[6, Bitboard], kind: PieceKind, bitboard: Bitboard) {.inline.} = self[kind.int] = bitboard
|
||||
|
||||
|
||||
func getBitboard*(self: Position, kind: PieceKind, color: PieceColor): Bitboard =
|
||||
## Returns the positional bitboard for the given piece kind and color
|
||||
return self.pieces[color.int][kind.int]
|
||||
|
||||
|
||||
func getBitboard*(self: Position, piece: Piece): Bitboard =
|
||||
## Returns the positional bitboard for the given piece type
|
||||
return self.getBitboard(piece.kind, piece.color)
|
|
@ -0,0 +1,72 @@
|
|||
# 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 bitboards
|
||||
import magics
|
||||
import pieces
|
||||
|
||||
|
||||
export bitboards, pieces
|
||||
|
||||
|
||||
# Stolen from https://github.com/Ciekce/voidstar/blob/main/src/rays.rs :D
|
||||
|
||||
|
||||
|
||||
proc computeRaysBetweenSquares: array[64, array[64, Bitboard]] =
|
||||
## Computes all sliding rays between each pair of squares
|
||||
## in the chessboard
|
||||
for i in 0..63:
|
||||
let
|
||||
source = Square(i)
|
||||
sourceBitboard = source.toBitboard()
|
||||
rooks = getRookMoves(source, Bitboard(0))
|
||||
bishops = getBishopMoves(source, Bitboard(0))
|
||||
for j in 0..63:
|
||||
let target = Square(j)
|
||||
if target == source:
|
||||
result[i][j] = Bitboard(0)
|
||||
else:
|
||||
let targetBitboard = target.toBitboard()
|
||||
if rooks.contains(target):
|
||||
result[i][j] = getRookMoves(source, targetBitboard) and getRookMoves(target, sourceBitboard)
|
||||
elif bishops.contains(target):
|
||||
result[i][j] = getBishopMoves(source, targetBitboard) and getBishopMoves(target, sourceBitboard)
|
||||
else:
|
||||
result[i][j] = Bitboard(0)
|
||||
|
||||
|
||||
let BETWEEN_RAYS = computeRaysBetweenSquares()
|
||||
|
||||
|
||||
proc getRayBetween*(source, target: Square): Bitboard {.inline.} = BETWEEN_RAYS[source.int][target.int]
|
||||
|
||||
proc queenSideCastleRay*(color: PieceColor): Bitboard {.inline.} =
|
||||
case color:
|
||||
of White:
|
||||
return getRayBetween("e1".toSquare(), "a1".toSquare())
|
||||
of Black:
|
||||
return getRayBetween("e8".toSquare(), "a8".toSquare())
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
proc kingSideCastleRay*(color: PieceColor): Bitboard {.inline.} =
|
||||
case color:
|
||||
of White:
|
||||
return getRayBetween("e1".toSquare(), "h1".toSquare())
|
||||
of Black:
|
||||
return getRayBetween("e8".toSquare(), "h8".toSquare())
|
||||
else:
|
||||
discard
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,462 @@
|
|||
# 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 movegen
|
||||
|
||||
|
||||
import std/strformat
|
||||
import std/strutils
|
||||
import std/times
|
||||
import std/math
|
||||
|
||||
from std/lenientops import `/`
|
||||
|
||||
|
||||
type
|
||||
CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
|
||||
|
||||
|
||||
proc perft*(board: Chessboard, ply: int, verbose: bool = false, divide: bool = false, bulk: bool = false): CountData =
|
||||
## Counts (and debugs) the number of legal positions reached after
|
||||
## the given number of ply
|
||||
|
||||
var moves = MoveList()
|
||||
board.generateMoves(moves)
|
||||
if not bulk:
|
||||
if len(moves) == 0 and board.inCheck():
|
||||
result.checkmates = 1
|
||||
# TODO: Should we count stalemates/draws?
|
||||
if ply == 0:
|
||||
result.nodes = 1
|
||||
return
|
||||
elif ply == 1 and bulk:
|
||||
if divide:
|
||||
var postfix = ""
|
||||
for move in moves:
|
||||
case move.getPromotionType():
|
||||
of PromoteToBishop:
|
||||
postfix = "b"
|
||||
of PromoteToKnight:
|
||||
postfix = "n"
|
||||
of PromoteToRook:
|
||||
postfix = "r"
|
||||
of PromoteToQueen:
|
||||
postfix = "q"
|
||||
else:
|
||||
postfix = ""
|
||||
echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: 1"
|
||||
if verbose:
|
||||
echo ""
|
||||
return (uint64(len(moves)), 0, 0, 0, 0, 0, 0)
|
||||
|
||||
for move in moves:
|
||||
if verbose:
|
||||
let canCastle = board.canCastle()
|
||||
echo &"Ply (from root): {board.position.plyFromRoot}"
|
||||
echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}"
|
||||
echo &"Turn: {board.position.sideToMove}"
|
||||
echo &"Piece: {board.getPiece(move.startSquare).kind}"
|
||||
echo &"Flags: {move.getFlags()}"
|
||||
echo &"In check: {(if board.inCheck(): \"yes\" else: \"no\")}"
|
||||
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||||
echo &"Position before move: {board.toFEN()}"
|
||||
stdout.write("En Passant target: ")
|
||||
if board.position.enPassantSquare != nullSquare():
|
||||
echo board.position.enPassantSquare.toAlgebraic()
|
||||
else:
|
||||
echo "None"
|
||||
echo "\n", board.pretty()
|
||||
board.doMove(move)
|
||||
if ply == 1:
|
||||
if move.isCapture():
|
||||
inc(result.captures)
|
||||
if move.isCastling():
|
||||
inc(result.castles)
|
||||
if move.isPromotion():
|
||||
inc(result.promotions)
|
||||
if move.isEnPassant():
|
||||
inc(result.enPassant)
|
||||
if board.inCheck():
|
||||
# Opponent king is in check
|
||||
inc(result.checks)
|
||||
if verbose:
|
||||
let canCastle = board.canCastle()
|
||||
echo "\n"
|
||||
echo &"Opponent in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
|
||||
echo &"Opponent can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||||
echo &"Position after move: {board.toFEN()}"
|
||||
echo "\n", board.pretty()
|
||||
stdout.write("nextpos>> ")
|
||||
try:
|
||||
discard readLine(stdin)
|
||||
except IOError:
|
||||
discard
|
||||
except EOFError:
|
||||
discard
|
||||
let next = board.perft(ply - 1, verbose, bulk=bulk)
|
||||
board.unmakeMove()
|
||||
if divide and (not bulk or ply > 1):
|
||||
var postfix = ""
|
||||
if move.isPromotion():
|
||||
case move.getPromotionType():
|
||||
of PromoteToBishop:
|
||||
postfix = "b"
|
||||
of PromoteToKnight:
|
||||
postfix = "n"
|
||||
of PromoteToRook:
|
||||
postfix = "r"
|
||||
of PromoteToQueen:
|
||||
postfix = "q"
|
||||
else:
|
||||
discard
|
||||
echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: {next.nodes}"
|
||||
if verbose:
|
||||
echo ""
|
||||
result.nodes += next.nodes
|
||||
result.captures += next.captures
|
||||
result.checks += next.checks
|
||||
result.promotions += next.promotions
|
||||
result.castles += next.castles
|
||||
result.enPassant += next.enPassant
|
||||
result.checkmates += next.checkmates
|
||||
|
||||
|
||||
proc handleGoCommand(board: Chessboard, command: seq[string]) =
|
||||
if len(command) < 2:
|
||||
echo &"Error: go: invalid number of arguments"
|
||||
return
|
||||
case command[1]:
|
||||
of "perft":
|
||||
if len(command) == 2:
|
||||
echo &"Error: go: perft: invalid number of arguments"
|
||||
return
|
||||
var
|
||||
args = command[2].splitWhitespace()
|
||||
bulk = false
|
||||
verbose = false
|
||||
if args.len() > 1:
|
||||
var ok = true
|
||||
for arg in args[1..^1]:
|
||||
case arg:
|
||||
of "bulk":
|
||||
bulk = true
|
||||
of "verbose":
|
||||
verbose = true
|
||||
else:
|
||||
echo &"Error: go: perft: invalid argument '{args[1]}'"
|
||||
ok = false
|
||||
break
|
||||
if not ok:
|
||||
return
|
||||
try:
|
||||
let ply = parseInt(args[0])
|
||||
if bulk:
|
||||
let t = cpuTime()
|
||||
let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes
|
||||
let tot = cpuTime() - t
|
||||
echo &"\nNodes searched (bulk-counting: on): {nodes}"
|
||||
echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(nodes / tot).uint64}"
|
||||
else:
|
||||
let t = cpuTime()
|
||||
let data = board.perft(ply, divide=true, verbose=verbose)
|
||||
let tot = cpuTime() - t
|
||||
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
|
||||
echo &" - Captures: {data.captures}"
|
||||
echo &" - Checks: {data.checks}"
|
||||
echo &" - E.P: {data.enPassant}"
|
||||
echo &" - Checkmates: {data.checkmates}"
|
||||
echo &" - Castles: {data.castles}"
|
||||
echo &" - Promotions: {data.promotions}"
|
||||
echo ""
|
||||
echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(data.nodes / tot).uint64}"
|
||||
except ValueError:
|
||||
echo "Error: go: perft: invalid depth"
|
||||
else:
|
||||
echo &"Error: go: unknown subcommand '{command[1]}'"
|
||||
|
||||
|
||||
proc handleMoveCommand(board: Chessboard, command: seq[string]): Move {.discardable.} =
|
||||
if len(command) != 2:
|
||||
echo &"Error: move: invalid number of arguments"
|
||||
return
|
||||
let moveString = command[1]
|
||||
if len(moveString) notin 4..5:
|
||||
echo &"Error: move: invalid move syntax"
|
||||
return
|
||||
var
|
||||
startSquare: Square
|
||||
targetSquare: Square
|
||||
flags: seq[MoveFlag]
|
||||
|
||||
try:
|
||||
startSquare = moveString[0..1].toSquare()
|
||||
except ValueError:
|
||||
echo &"Error: move: invalid start square ({moveString[0..1]})"
|
||||
return
|
||||
try:
|
||||
targetSquare = moveString[2..3].toSquare()
|
||||
except ValueError:
|
||||
echo &"Error: move: invalid target square ({moveString[2..3]})"
|
||||
return
|
||||
|
||||
# Since the user tells us just the source and target square of the move,
|
||||
# we have to figure out all the flags by ourselves (whether it's a double
|
||||
# push, a capture, a promotion, etc.)
|
||||
|
||||
if board.getPiece(targetSquare).kind != Empty:
|
||||
flags.add(Capture)
|
||||
|
||||
if board.getPiece(startSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2:
|
||||
flags.add(DoublePush)
|
||||
|
||||
if len(moveString) == 5:
|
||||
# Promotion
|
||||
case moveString[4]:
|
||||
of 'b':
|
||||
flags.add(PromoteToBishop)
|
||||
of 'n':
|
||||
flags.add(PromoteToKnight)
|
||||
of 'q':
|
||||
flags.add(PromoteToQueen)
|
||||
of 'r':
|
||||
flags.add(PromoteToRook)
|
||||
else:
|
||||
echo &"Error: move: invalid promotion type"
|
||||
return
|
||||
|
||||
|
||||
var move = createMove(startSquare, targetSquare, flags)
|
||||
let piece = board.getPiece(move.startSquare)
|
||||
if piece.kind == King and move.startSquare == board.position.sideToMove.getKingStartingSquare():
|
||||
if move.targetSquare in [piece.kingSideCastling(), piece.queenSideCastling()]:
|
||||
move.flags = move.flags or Castle.uint16
|
||||
elif move.targetSquare == board.position.enPassantSquare:
|
||||
move.flags = move.flags or EnPassant.uint16
|
||||
result = board.makeMove(move)
|
||||
if result == nullMove():
|
||||
echo &"Error: move: {moveString} is illegal"
|
||||
|
||||
|
||||
proc handlePositionCommand(board: var Chessboard, command: seq[string]) =
|
||||
if len(command) < 2:
|
||||
echo "Error: position: invalid number of arguments"
|
||||
return
|
||||
# Makes sure we don't leave the board in an invalid state if
|
||||
# some error occurs
|
||||
var tempBoard: Chessboard
|
||||
case command[1]:
|
||||
of "startpos", "kiwipete":
|
||||
if command[1] == "kiwipete":
|
||||
tempBoard = newChessboardFromFen("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -")
|
||||
else:
|
||||
tempBoard = newDefaultChessboard()
|
||||
if command.len() > 2:
|
||||
let args = command[2].splitWhitespace()
|
||||
if args.len() > 0:
|
||||
var i = 0
|
||||
while i < args.len():
|
||||
case args[i]:
|
||||
of "moves":
|
||||
var j = i + 1
|
||||
while j < args.len():
|
||||
if handleMoveCommand(tempBoard, @["move", args[j]]) == nullMove():
|
||||
return
|
||||
inc(j)
|
||||
inc(i)
|
||||
board = tempBoard
|
||||
of "fen":
|
||||
if len(command) == 2:
|
||||
echo &"Current position: {board.toFEN()}"
|
||||
return
|
||||
var
|
||||
args = command[2].splitWhitespace()
|
||||
fenString = ""
|
||||
stop = 0
|
||||
for i, arg in args:
|
||||
if arg in ["moves", ]:
|
||||
break
|
||||
if i > 0:
|
||||
fenString &= " "
|
||||
fenString &= arg
|
||||
inc(stop)
|
||||
args = args[stop..^1]
|
||||
try:
|
||||
tempBoard = newChessboardFromFEN(fenString)
|
||||
except ValueError:
|
||||
echo &"error: position: {getCurrentExceptionMsg()}"
|
||||
return
|
||||
if args.len() > 0:
|
||||
var i = 0
|
||||
while i < args.len():
|
||||
case args[i]:
|
||||
of "moves":
|
||||
var j = i + 1
|
||||
while j < args.len():
|
||||
if handleMoveCommand(tempBoard, @["move", args[j]]) == nullMove():
|
||||
return
|
||||
inc(j)
|
||||
inc(i)
|
||||
board = tempBoard
|
||||
of "print":
|
||||
echo board
|
||||
of "pretty":
|
||||
echo board.pretty()
|
||||
else:
|
||||
echo &"error: position: unknown subcommand '{command[1]}'"
|
||||
return
|
||||
|
||||
|
||||
proc handleUCICommand(board: var Chessboard, command: seq[string]) =
|
||||
echo "id name Nimfish 0.1"
|
||||
echo "id author Nocturn9x & Contributors (see LICENSE)"
|
||||
# TODO
|
||||
echo "uciok"
|
||||
|
||||
|
||||
const HELP_TEXT = """Nimfish help menu:
|
||||
- go: Begin a search
|
||||
Subcommands:
|
||||
- perft <depth> [options]: Run the performance test at the given depth (in ply) and
|
||||
print the results
|
||||
Options:
|
||||
- bulk: Enable bulk-counting (significantly faster, gives less statistics)
|
||||
- verbose: Enable move debugging (for each and every move, not recommended on large searches)
|
||||
Example: go perft 5 bulk
|
||||
- position: Get/set board position
|
||||
Subcommands:
|
||||
- fen [string]: Set the board to the given fen string if one is provided, or print
|
||||
the current position as a FEN string if no arguments are given
|
||||
- startpos: Set the board to the starting position
|
||||
- kiwipete: Set the board to famous kiwipete position
|
||||
- pretty: Pretty-print the current position
|
||||
- print: Print the current position using ASCII characters only
|
||||
Options:
|
||||
- moves {moveList}: Perform the given moves (space-separated, all-lowercase)
|
||||
in algebraic notation after the position is loaded. This option only applies
|
||||
to the subcommands that set a position, it is ignored otherwise
|
||||
Examples:
|
||||
- position startpos
|
||||
- position fen "..." moves a2a3 a7a6
|
||||
- clear: Clear the screen
|
||||
- move <move>: Perform the given move in algebraic notation
|
||||
- castle: Print castling rights for the side to move
|
||||
- check: Print if the current side to move is in check
|
||||
- unmove, u: Unmakes the last move. Can be used in succession
|
||||
- stm: Print which side is to move
|
||||
- ep: Print the current en passant target
|
||||
- pretty: Shorthand for "position pretty"
|
||||
- print: Shorthand for "position print"
|
||||
- fen: Shorthand for "position fen"
|
||||
- pos <args>: Shorthand for "position <args>"
|
||||
- get <square>: Get the piece on the given square
|
||||
- atk <square>: Print which pieces are currently attacking the given square
|
||||
- pins: Print the current pin mask
|
||||
- checks: Print the current checks mask
|
||||
- skip: Swap the side to move
|
||||
- uci: enter UCI mode (WIP)
|
||||
- quit: exit
|
||||
"""
|
||||
|
||||
|
||||
proc commandLoop*: int =
|
||||
## Nimfish's control interface
|
||||
echo "Nimfish by nocturn9x (see LICENSE)"
|
||||
var
|
||||
board = newDefaultChessboard()
|
||||
uciMode = false
|
||||
while true:
|
||||
var
|
||||
cmd: seq[string]
|
||||
cmdStr: string
|
||||
try:
|
||||
if not uciMode:
|
||||
stdout.write(">>> ")
|
||||
stdout.flushFile()
|
||||
cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '})
|
||||
if cmdStr.len() == 0:
|
||||
continue
|
||||
cmd = cmdStr.splitWhitespace(maxsplit=2)
|
||||
|
||||
case cmd[0]:
|
||||
of "uci":
|
||||
handleUCICommand(board, cmd)
|
||||
uciMode = true
|
||||
of "clear":
|
||||
echo "\x1Bc"
|
||||
of "help":
|
||||
echo HELP_TEXT
|
||||
of "skip":
|
||||
board.position.sideToMove = board.position.sideToMove.opposite()
|
||||
board.updateChecksAndPins()
|
||||
of "go":
|
||||
handleGoCommand(board, cmd)
|
||||
of "position", "pos":
|
||||
handlePositionCommand(board, cmd)
|
||||
of "move":
|
||||
handleMoveCommand(board, cmd)
|
||||
of "pretty", "print", "fen":
|
||||
handlePositionCommand(board, @["position", cmd[0]])
|
||||
of "unmove", "u":
|
||||
if board.positions.len() == 0:
|
||||
echo "No previous move to undo"
|
||||
else:
|
||||
board.unmakeMove()
|
||||
of "stm":
|
||||
echo &"Side to move: {board.position.sideToMove}"
|
||||
of "atk":
|
||||
if len(cmd) != 2:
|
||||
echo "error: atk: invalid number of arguments"
|
||||
continue
|
||||
try:
|
||||
echo board.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove.opposite())
|
||||
except ValueError:
|
||||
echo "error: atk: invalid square"
|
||||
continue
|
||||
of "ep":
|
||||
let target = board.position.enPassantSquare
|
||||
if target != nullSquare():
|
||||
echo &"En passant target: {target.toAlgebraic()}"
|
||||
else:
|
||||
echo "En passant target: None"
|
||||
of "get":
|
||||
if len(cmd) != 2:
|
||||
echo "error: get: invalid number of arguments"
|
||||
continue
|
||||
try:
|
||||
echo board.getPiece(cmd[1])
|
||||
except ValueError:
|
||||
echo "error: get: invalid square"
|
||||
continue
|
||||
of "castle":
|
||||
let canCastle = board.canCastle()
|
||||
echo &"Castling rights for {($board.position.sideToMove).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||||
of "check":
|
||||
echo &"{board.position.sideToMove} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
|
||||
of "pins":
|
||||
if board.position.orthogonalPins != 0:
|
||||
echo &"Orthogonal pins:\n{board.position.orthogonalPins}"
|
||||
if board.position.diagonalPins != 0:
|
||||
echo &"Diagonal pins:\n{board.position.diagonalPins}"
|
||||
of "checks":
|
||||
echo board.position.checkers
|
||||
of "quit":
|
||||
return 0
|
||||
else:
|
||||
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
|
||||
except IOError:
|
||||
echo ""
|
||||
return 0
|
||||
except EOFError:
|
||||
echo ""
|
||||
return 0
|
|
@ -10,6 +10,8 @@ from argparse import ArgumentParser, Namespace
|
|||
def main(args: Namespace) -> int:
|
||||
if args.silent:
|
||||
print = lambda *_: ...
|
||||
else:
|
||||
print = __builtins__.print
|
||||
print("Nimfish move validator v0.0.1 by nocturn9x")
|
||||
try:
|
||||
STOCKFISH = (args.stockfish or Path(which("stockfish"))).resolve(strict=True)
|
||||
|
@ -17,7 +19,7 @@ def main(args: Namespace) -> int:
|
|||
print(f"Could not locate stockfish executable -> {type(e).__name__}: {e}")
|
||||
return 2
|
||||
try:
|
||||
NIMFISH = (args.nimfish or (Path.cwd() / "bin" / "nimfish")).resolve(strict=True)
|
||||
NIMFISH = (args.nimfish or Path(which("nimfish"))).resolve(strict=True)
|
||||
except Exception as e:
|
||||
print(f"Could not locate nimfish executable -> {type(e).__name__}: {e}")
|
||||
return 2
|
||||
|
@ -164,4 +166,7 @@ if __name__ == "__main__":
|
|||
parser.add_argument("--stockfish", type=Path, help="Path to the stockfish executable. Defaults to '' (detected automatically)", default=None)
|
||||
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None)
|
||||
parser.add_argument("--silent", action="store_true", help="Disable all output (a return code of 0 means the test was successful)", default=False)
|
||||
sys.exit(main(parser.parse_args()))
|
||||
try:
|
||||
sys.exit(main(parser.parse_args()))
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(255)
|
|
@ -1,3 +1,6 @@
|
|||
1B6/8/8/8/5pP1/8/7k/4K3 b - g3 0 1
|
||||
1k6/8/8/8/4Pp2/8/7B/4K3 b - e3 0 1
|
||||
1k6/8/8/8/5pP1/8/7B/4K3 b - g3 0 1
|
||||
1r2k2r/8/8/8/8/8/8/R3K2R b KQk - 0 1
|
||||
1r2k2r/8/8/8/8/8/8/R3K2R w KQk - 0 1
|
||||
2K2r2/4P3/8/8/8/8/8/3k4 w - - 0 1
|
||||
|
@ -14,21 +17,56 @@
|
|||
4k2r/8/8/8/8/8/8/4K3 b k - 0 1
|
||||
4k2r/8/8/8/8/8/8/4K3 w k - 0 1
|
||||
4k3/1P6/8/8/8/8/K7/8 w - - 0 1
|
||||
4k3/2rn4/8/2K1pP2/8/8/8/8 w - e6 0 1
|
||||
4k3/4p3/4K3/8/8/8/8/8 b - - 0 1
|
||||
4k3/7K/8/5Pp1/8/8/8/1b6 w - g6 0 1
|
||||
4k3/7b/8/4pP2/4K3/8/8/8 w - e6 0 1
|
||||
4k3/7b/8/4pP2/8/8/8/1K6 w - e6 0 1
|
||||
4k3/7b/8/5Pp1/8/8/8/1K6 w - g6 0 1
|
||||
4k3/8/1b6/2Pp4/3K4/8/8/8 w - d6 0 1
|
||||
4k3/8/3K4/1pP5/8/q7/8/8 w - b6 0 1
|
||||
4k3/8/4q3/8/8/8/3b4/4K3 w - - 0 1
|
||||
4k3/8/4r3/8/8/8/3p4/4K3 w - - 0 1
|
||||
4k3/8/6b1/4pP2/4K3/8/8/8 w - e6 0 1
|
||||
4k3/8/7b/5pP1/5K2/8/8/8 w - f6 0 1
|
||||
4k3/8/8/2PpP3/8/8/8/4K3 w - d6 0 1
|
||||
4k3/8/8/4pP2/3K4/8/8/8 w - e6 0 1
|
||||
4k3/8/8/8/1b2r3/8/3Q4/4K3 w - - 0 1
|
||||
4k3/8/8/8/1b2r3/8/3QP3/4K3 w - - 0 1
|
||||
4k3/8/8/8/1b5b/2Q5/5P2/4K3 w - - 0 1
|
||||
4k3/8/8/8/1b5b/2R5/5P2/4K3 w - - 0 1
|
||||
4k3/8/8/8/1b5b/8/3Q4/4K3 w - - 0 1
|
||||
4k3/8/8/8/1b5b/8/3R4/4K3 w - - 0 1
|
||||
4k3/8/8/8/2pPp3/8/8/4K3 b - d3 0 1
|
||||
4k3/8/8/8/8/8/8/4K2R b K - 0 1
|
||||
4k3/8/8/8/8/8/8/4K2R w K - 0 1
|
||||
4k3/8/8/8/8/8/8/R3K2R b KQ - 0 1
|
||||
4k3/8/8/8/8/8/8/R3K2R w KQ - 0 1
|
||||
4k3/8/8/8/8/8/8/R3K3 b Q - 0 1
|
||||
4k3/8/8/8/8/8/8/R3K3 w Q - 0 1
|
||||
4k3/8/8/K2pP2q/8/8/8/8 w - d6 0 1
|
||||
4k3/8/8/K2pP2r/8/8/8/8 w - d6 0 1
|
||||
4k3/8/8/q2pP2K/8/8/8/8 w - d6 0 1
|
||||
4k3/8/8/r2pP2K/8/8/8/8 w - d6 0 1
|
||||
4k3/8/K6q/3pP3/8/8/8/8 w - d6 0 1
|
||||
4k3/8/K6r/3pP3/8/8/8/8 w - d6 0 1
|
||||
4k3/8/b7/1Pp5/2K5/8/8/8 w - c6 0 1
|
||||
4k3/K7/8/1pP5/8/8/8/6b1 w - b6 0 1
|
||||
4k3/b7/8/1pP5/8/8/8/6K1 w - b6 0 1
|
||||
4k3/b7/8/2Pp4/3K4/8/8/8 w - d6 0 1
|
||||
4k3/b7/8/2Pp4/8/8/8/6K1 w - d6 0 1
|
||||
5k2/8/8/8/8/8/8/4K2R w K - 0 1
|
||||
6B1/8/8/8/1Pp5/8/k7/4K3 b - b3 0 1
|
||||
6KQ/8/8/8/8/8/8/7k b - - 0 1
|
||||
6k1/8/8/8/1Pp5/8/B7/4K3 b - b3 0 1
|
||||
6k1/8/8/8/2pP4/8/B7/3K4 b - d3 0 1
|
||||
6kq/8/8/8/8/8/8/7K w - - 0 1
|
||||
6qk/8/8/8/8/8/8/7K b - - 0 1
|
||||
7k/3p4/8/8/3P4/8/8/K7 b - - 0 1
|
||||
7k/3p4/8/8/3P4/8/8/K7 w - - 0 1
|
||||
7K/7p/7k/8/8/8/8/8 b - - 0 1
|
||||
7K/7p/7k/8/8/8/8/8 w - - 0 1
|
||||
7k/3p4/8/8/3P4/8/8/K7 b - - 0 1
|
||||
7k/3p4/8/8/3P4/8/8/K7 w - - 0 1
|
||||
7k/4K3/8/1pP5/8/q7/8/8 w - b6 0 1
|
||||
7k/8/1p6/8/8/P7/8/7K b - - 0 1
|
||||
7k/8/1p6/8/8/P7/8/7K w - - 0 1
|
||||
7k/8/8/1p6/P7/8/8/7K b - - 0 1
|
||||
|
@ -52,8 +90,8 @@
|
|||
8/3k4/3p4/8/3P4/3K4/8/8 w - - 0 1
|
||||
8/8/1B6/7b/7k/8/2B1b3/7K b - - 0 1
|
||||
8/8/1B6/7b/7k/8/2B1b3/7K w - - 0 1
|
||||
8/8/1k6/2b5/2pP4/8/5K2/8 b - d3 0 1
|
||||
8/8/1P2K3/8/2n5/1q6/8/5k2 b - - 0 1
|
||||
8/8/1k6/2b5/2pP4/8/5K2/8 b - d3 0 1
|
||||
8/8/2k5/5q2/5n2/8/5K2/8 b - - 0 1
|
||||
8/8/3K4/3Nn3/3nN3/4k3/8/8 b - - 0 1
|
||||
8/8/3k4/3p4/3P4/3K4/8/8 b - - 0 1
|
||||
|
@ -65,6 +103,11 @@
|
|||
8/8/7k/7p/7P/7K/8/8 b - - 0 1
|
||||
8/8/7k/7p/7P/7K/8/8 w - - 0 1
|
||||
8/8/8/2k5/2pP4/8/B7/4K3 b - d3 0 3
|
||||
8/8/8/4k3/5Pp1/8/8/3K4 b - f3 0 1
|
||||
8/8/8/8/1R1Pp2k/8/8/4K3 b - d3 0 1
|
||||
8/8/8/8/1k1Pp2R/8/8/4K3 b - d3 0 1
|
||||
8/8/8/8/1k1PpN1R/8/8/4K3 b - d3 0 1
|
||||
8/8/8/8/1k1Ppn1R/8/8/4K3 b - d3 0 1
|
||||
8/8/8/8/8/4k3/4P3/4K3 w - - 0 1
|
||||
8/8/8/8/8/7K/7P/7k b - - 0 1
|
||||
8/8/8/8/8/7K/7P/7k w - - 0 1
|
||||
|
@ -76,45 +119,49 @@
|
|||
8/8/8/8/8/K7/P7/k7 w - - 0 1
|
||||
8/8/k7/p7/P7/K7/8/8 b - - 0 1
|
||||
8/8/k7/p7/P7/K7/8/8 w - - 0 1
|
||||
8/k1P5/8/1K6/8/8/8/8 w - - 0 1
|
||||
8/P1k5/K7/8/8/8/8/8 w - - 0 1
|
||||
8/Pk6/8/8/8/8/6Kp/8 b - - 0 1
|
||||
8/Pk6/8/8/8/8/6Kp/8 w - - 0 1
|
||||
8/PPPk4/8/8/8/8/4Kppp/8 b - - 0 1
|
||||
8/PPPk4/8/8/8/8/4Kppp/8 w - - 0 1
|
||||
8/Pk6/8/8/8/8/6Kp/8 b - - 0 1
|
||||
8/Pk6/8/8/8/8/6Kp/8 w - - 0 1
|
||||
8/k1P5/8/1K6/8/8/8/8 w - - 0 1
|
||||
B6b/8/8/8/2K5/4k3/8/b6B w - - 0 1
|
||||
B6b/8/8/8/2K5/5k2/8/b6B b - - 0 1
|
||||
K1k5/8/P7/8/8/8/8/8 w - - 0 1
|
||||
K7/8/2n5/1n6/8/8/8/k6N b - - 0 1
|
||||
K7/8/2n5/1n6/8/8/8/k6N w - - 0 1
|
||||
K7/8/8/3Q4/4q3/8/8/7k b - - 0 1
|
||||
K7/8/8/3Q4/4q3/8/8/7k w - - 0 1
|
||||
K7/b7/1b6/1b6/8/8/8/k6B b - - 0 1
|
||||
K7/b7/1b6/1b6/8/8/8/k6B w - - 0 1
|
||||
K7/p7/k7/8/8/8/8/8 b - - 0 1
|
||||
K7/p7/k7/8/8/8/8/8 w - - 0 1
|
||||
R6r/8/8/2K5/5k2/8/8/r6R b - - 0 1
|
||||
R6r/8/8/2K5/5k2/8/8/r6R w - - 0 1
|
||||
k3K3/8/8/3pP3/8/8/8/4r3 w - d6 0 1
|
||||
k7/6p1/8/8/8/8/7P/K7 b - - 0 1
|
||||
k7/6p1/8/8/8/8/7P/K7 w - - 0 1
|
||||
k7/7p/8/8/8/8/6P1/K7 b - - 0 1
|
||||
k7/7p/8/8/8/8/6P1/K7 w - - 0 1
|
||||
K7/8/2n5/1n6/8/8/8/k6N b - - 0 1
|
||||
k7/8/2N5/1N6/8/8/8/K6n b - - 0 1
|
||||
K7/8/2n5/1n6/8/8/8/k6N w - - 0 1
|
||||
k7/8/2N5/1N6/8/8/8/K6n w - - 0 1
|
||||
k7/8/3p4/8/3P4/8/8/7K b - - 0 1
|
||||
k7/8/3p4/8/3P4/8/8/7K w - - 0 1
|
||||
k7/8/3p4/8/8/4P3/8/7K b - - 0 1
|
||||
k7/8/3p4/8/8/4P3/8/7K w - - 0 1
|
||||
k7/8/4r3/3pP3/8/8/8/4K3 w - d6 0 1
|
||||
k7/8/6p1/8/8/7P/8/K7 b - - 0 1
|
||||
k7/8/6p1/8/8/7P/8/K7 w - - 0 1
|
||||
k7/8/7p/8/8/6P1/8/K7 b - - 0 1
|
||||
k7/8/7p/8/8/6P1/8/K7 w - - 0 1
|
||||
k7/8/8/3p4/4p3/8/8/7K b - - 0 1
|
||||
k7/8/8/3p4/4p3/8/8/7K w - - 0 1
|
||||
K7/8/8/3Q4/4q3/8/8/7k b - - 0 1
|
||||
K7/8/8/3Q4/4q3/8/8/7k w - - 0 1
|
||||
k7/8/8/6p1/7P/8/8/K7 b - - 0 1
|
||||
k7/8/8/6p1/7P/8/8/K7 w - - 0 1
|
||||
k7/8/8/7p/6P1/8/8/K7 b - - 0 1
|
||||
k7/8/8/7p/6P1/8/8/K7 w - - 0 1
|
||||
k7/B7/1B6/1B6/8/8/8/K6b b - - 0 1
|
||||
K7/b7/1b6/1b6/8/8/8/k6B b - - 0 1
|
||||
k7/B7/1B6/1B6/8/8/8/K6b w - - 0 1
|
||||
K7/b7/1b6/1b6/8/8/8/k6B w - - 0 1
|
||||
K7/p7/k7/8/8/8/8/8 b - - 0 1
|
||||
K7/p7/k7/8/8/8/8/8 w - - 0 1
|
||||
n1n5/1Pk5/8/8/8/8/5Kp1/5N1N b - - 0 1
|
||||
n1n5/1Pk5/8/8/8/8/5Kp1/5N1N w - - 0 1
|
||||
n1n5/PPPk4/8/8/8/8/4Kppp/5N1N b - - 0 1
|
||||
|
@ -142,8 +189,6 @@ r3k3/8/8/8/8/8/8/4K3 b q - 0 1
|
|||
r3k3/8/8/8/8/8/8/4K3 w q - 0 1
|
||||
r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10
|
||||
r6r/1b2k1bq/8/8/7B/8/8/R3K2R b KQ - 3 2
|
||||
R6r/8/8/2K5/5k2/8/8/r6R b - - 0 1
|
||||
R6r/8/8/2K5/5k2/8/8/r6R w - - 0 1
|
||||
rnb2k1r/pp1Pbppp/2p5/q7/2B5/8/PPPQNnPP/RNB1K2R w KQ - 3 9
|
||||
rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8
|
||||
rnbqkb1r/ppppp1pp/7n/4Pp2/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3
|
|
@ -0,0 +1,84 @@
|
|||
import sys
|
||||
import timeit
|
||||
from pathlib import Path
|
||||
from argparse import Namespace, ArgumentParser
|
||||
from compare_positions import main as test
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from multiprocessing import cpu_count
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
def main(args: Namespace) -> int:
|
||||
# We try to be polite with resource usage
|
||||
if not args.parallel:
|
||||
print("[S] Starting test suite")
|
||||
else:
|
||||
print(f"[S] Starting test suite with {args.workers} workers")
|
||||
successful = []
|
||||
failed = []
|
||||
positions = args.positions.read_text().splitlines()
|
||||
longest_fen = max(sorted([len(fen) for fen in positions]))
|
||||
start = timeit.default_timer()
|
||||
if not args.parallel:
|
||||
for i, fen in enumerate(positions):
|
||||
fen = fen.strip(" ")
|
||||
fen += " " * (longest_fen - len(fen))
|
||||
sys.stdout.write(f"\r[S] Testing {fen} ({i + 1}/{len(positions)})\033[K")
|
||||
args.fen = fen
|
||||
args.silent = not args.no_silent
|
||||
if test(args) == 0:
|
||||
successful.append(fen)
|
||||
else:
|
||||
failed.append(fen)
|
||||
else:
|
||||
# There is no compute going on in the Python thread,
|
||||
# it's just I/O waiting for the processes to finish,
|
||||
# so using a thread as opposed to a process doesn't
|
||||
# make much different w.r.t. the GIL (and threads are
|
||||
# cheaper than processes on some platforms)
|
||||
futures = {}
|
||||
try:
|
||||
pool = ThreadPoolExecutor(args.workers)
|
||||
for fen in positions:
|
||||
args = deepcopy(args)
|
||||
args.fen = fen.strip(" ")
|
||||
args.silent = not args.no_silent
|
||||
futures[pool.submit(test, args)] = args.fen
|
||||
for i, future in enumerate(as_completed(futures)):
|
||||
sys.stdout.write(f"\r[S] Testing in progress ({i + 1}/{len(positions)})\033[K")
|
||||
if future.result() == 0:
|
||||
successful.append(futures[future])
|
||||
else:
|
||||
failed.append(futures[future])
|
||||
except KeyboardInterrupt:
|
||||
stop = timeit.default_timer()
|
||||
pool.shutdown(cancel_futures=True)
|
||||
print(f"\r[S] Interrupted\033[K")
|
||||
print(f"[S] Ran {i} tests at depth {args.ply} in {stop - start:.2f} seconds ({len(successful)} successful, {len(failed)} failed)")
|
||||
if failed and args.show_failures:
|
||||
print("[S] The following FENs failed to pass the test:\n\t", end="")
|
||||
print("\n\t".join(failed))
|
||||
else:
|
||||
stop = timeit.default_timer()
|
||||
print(f"\r[S] Ran {len(positions)} tests at depth {args.ply} in {stop - start:.2f} seconds ({len(successful)} successful, {len(failed)} failed)\033[K")
|
||||
if failed and args.show_failures:
|
||||
print("[S] The following FENs failed to pass the test:\n\t", end="")
|
||||
print("\n\t".join(failed))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser(description="Run a set of tests using compare_positions.py")
|
||||
parser.add_argument("--ply", "-d", type=int, required=True, help="The depth to stop at, expressed in plys (half-moves)")
|
||||
parser.add_argument("-b", "--bulk", action="store_true", help="Enable bulk-counting for Nimfish (much faster)", default=False)
|
||||
parser.add_argument("--stockfish", type=Path, help="Path to the stockfish executable. Defaults to '' (detected automatically)", default=None)
|
||||
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None)
|
||||
parser.add_argument("--positions", type=Path, help="Location of the file containing FENs to test, one per line. Defaults to 'tests/positions.txt'",
|
||||
default=Path("tests/positions.txt"))
|
||||
parser.add_argument("--no-silent", action="store_true", help="Do not suppress output from compare_positions.py (defaults to False)", default=False)
|
||||
parser.add_argument("-p", "--parallel", action="store_true", help="Run multiple tests in parallel", default=False)
|
||||
parser.add_argument("--workers", "-w", type=int, required=False, help="How many workers to use in parallel mode (defaults to cpu_count() / 2)", default=cpu_count() // 2)
|
||||
parser.add_argument("-s", "--show-failures", action="store_true", help="Show which FENs failed to pass the test", default=False)
|
||||
try:
|
||||
sys.exit(main(parser.parse_args()))
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(255)
|
|
@ -12,6 +12,6 @@ in semiconductor technology smart enough to play tic tac toe.
|
|||
- Chess -> WIP
|
||||
|
||||
|
||||
All of these games will be played using decision trees searched using the minimax algorithm (maybe a bit of neural networks too, who knows).
|
||||
All of these games will be played using decision trees searched using the minimax algorithm or variations thereof (maybe a bit of neural networks too, who knows).
|
||||
Ideally I'd like to implement a bunch of stuff such as move reordering, alpha-beta pruning and transpositions in order to improve both
|
||||
processing time and decision quality. Very much WIP.
|
2352
src/Chess/board.nim
2352
src/Chess/board.nim
File diff suppressed because it is too large
Load Diff
|
@ -1,60 +0,0 @@
|
|||
import board as chess
|
||||
import std/strformat
|
||||
import std/strutils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
when isMainModule:
|
||||
setControlCHook(proc () {.noconv.} = echo ""; quit(0))
|
||||
const fen = "rnbqkbnr/2p/8/8/8/8/P7/RNBQKBNR w KQkq - 0 1"
|
||||
var
|
||||
board = newChessboardFromFEN(fen)
|
||||
canCastle: tuple[queen, king: bool]
|
||||
data: string
|
||||
move: Move
|
||||
|
||||
echo "\x1Bc"
|
||||
while true:
|
||||
canCastle = board.canCastle()
|
||||
echo &"{board.pretty()}"
|
||||
echo &"Turn: {board.getActiveColor()}"
|
||||
echo &"Moves: {board.getMoveCount()} full, {board.getHalfMoveCount()} half"
|
||||
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||||
stdout.write(&"En passant target: ")
|
||||
if board.getEnPassantTarget() != emptyLocation():
|
||||
echo board.getEnPassantTarget().locationToAlgebraic()
|
||||
else:
|
||||
echo "None"
|
||||
stdout.write(&"Check: ")
|
||||
if board.inCheck():
|
||||
echo &"Yes"
|
||||
else:
|
||||
echo "No"
|
||||
stdout.write("\nMove(s) -> ")
|
||||
try:
|
||||
data = readLine(stdin).strip(chars={'\0', ' '})
|
||||
except IOError:
|
||||
echo ""
|
||||
break
|
||||
if data == "undo":
|
||||
echo &"\x1BcUndo: {board.undoLastMove()}"
|
||||
continue
|
||||
if data == "reset":
|
||||
echo &"\x1BcBoard reset"
|
||||
board = newChessboardFromFEN(fen)
|
||||
continue
|
||||
for moveChars in data.split(" "):
|
||||
if len(moveChars) != 4:
|
||||
echo "\x1BcError: invalid move"
|
||||
break
|
||||
try:
|
||||
move = board.makeMove(moveChars[0..1], moveChars[2..3])
|
||||
except ValueError:
|
||||
echo &"\x1BcError: {getCurrentExceptionMsg()}"
|
||||
if move == emptyMove():
|
||||
echo &"\x1BcError: move '{moveChars}' is illegal"
|
||||
break
|
||||
else:
|
||||
echo "\x1Bc"
|
|
@ -1,43 +0,0 @@
|
|||
import sys
|
||||
import timeit
|
||||
from pathlib import Path
|
||||
from argparse import Namespace, ArgumentParser
|
||||
from compare_positions import main as test
|
||||
|
||||
|
||||
|
||||
def main(args: Namespace) -> int:
|
||||
print("[S] Starting test suite")
|
||||
successful = []
|
||||
failed = []
|
||||
positions = args.positions.read_text().splitlines()
|
||||
start = timeit.default_timer()
|
||||
longest_fen = max(sorted([len(fen) for fen in positions]))
|
||||
for i, fen in enumerate(positions):
|
||||
fen = fen.strip(" ")
|
||||
fen += " " * (longest_fen - len(fen))
|
||||
sys.stdout.write(f"\r[S] Testing {fen} ({i + 1}/{len(positions)})\033[K")
|
||||
args.fen = fen
|
||||
args.silent = not args.no_silent
|
||||
if test(args) == 0:
|
||||
successful.append(fen)
|
||||
else:
|
||||
failed.append(fen)
|
||||
stop = timeit.default_timer()
|
||||
print(f"\r[S] Ran {len(positions)} tests at depth {args.ply} in {stop - start:.2f} seconds ({len(successful)} successful, {len(failed)} failed)\033[K")
|
||||
if failed and args.show_failures:
|
||||
print("[S] The following FENs failed to pass the test:", end="\n\t")
|
||||
print("\n\t".join(failed))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser(description="Run a set of tests using compare_positions.py")
|
||||
parser.add_argument("--ply", "-d", type=int, required=True, help="The depth to stop at, expressed in plys (half-moves)")
|
||||
parser.add_argument("--bulk", action="store_true", help="Enable bulk-counting for Nimfish (much faster)", default=False)
|
||||
parser.add_argument("--stockfish", type=Path, help="Path to the stockfish executable. Defaults to '' (detected automatically)", default=None)
|
||||
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None)
|
||||
parser.add_argument("--positions", type=Path, help="Location of the file containing FENs to test, one per line. Defaults to 'tests/positions.txt'",
|
||||
default=Path("tests/positions.txt"))
|
||||
parser.add_argument("--no-silent", action="store_true", help="Do not suppress output from compare_positions.py (defaults)", default=False)
|
||||
parser.add_argument("--show-failures", action="store_true", help="Show which FENs failed to pass the test", default=False)
|
||||
sys.exit(main(parser.parse_args()))
|
Loading…
Reference in New Issue