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/
|
nimcache/
|
||||||
nimblecache/
|
nimblecache/
|
||||||
htmldocs/
|
htmldocs/
|
||||||
nim.cfg
|
|
||||||
bin
|
bin
|
||||||
# Python
|
# Python
|
||||||
__pycache__
|
__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:
|
def main(args: Namespace) -> int:
|
||||||
if args.silent:
|
if args.silent:
|
||||||
print = lambda *_: ...
|
print = lambda *_: ...
|
||||||
|
else:
|
||||||
|
print = __builtins__.print
|
||||||
print("Nimfish move validator v0.0.1 by nocturn9x")
|
print("Nimfish move validator v0.0.1 by nocturn9x")
|
||||||
try:
|
try:
|
||||||
STOCKFISH = (args.stockfish or Path(which("stockfish"))).resolve(strict=True)
|
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}")
|
print(f"Could not locate stockfish executable -> {type(e).__name__}: {e}")
|
||||||
return 2
|
return 2
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
print(f"Could not locate nimfish executable -> {type(e).__name__}: {e}")
|
print(f"Could not locate nimfish executable -> {type(e).__name__}: {e}")
|
||||||
return 2
|
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("--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("--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)
|
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 b KQk - 0 1
|
||||||
1r2k2r/8/8/8/8/8/8/R3K2R w 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
|
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 b k - 0 1
|
||||||
4k2r/8/8/8/8/8/8/4K3 w 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/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/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 b K - 0 1
|
||||||
4k3/8/8/8/8/8/8/4K2R w 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 b KQ - 0 1
|
||||||
4k3/8/8/8/8/8/8/R3K2R w 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 b Q - 0 1
|
||||||
4k3/8/8/8/8/8/8/R3K3 w 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
|
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
|
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
|
6kq/8/8/8/8/8/8/7K w - - 0 1
|
||||||
6qk/8/8/8/8/8/8/7K b - - 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 b - - 0 1
|
||||||
7K/7p/7k/8/8/8/8/8 w - - 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 b - - 0 1
|
||||||
7k/8/1p6/8/8/P7/8/7K w - - 0 1
|
7k/8/1p6/8/8/P7/8/7K w - - 0 1
|
||||||
7k/8/8/1p6/P7/8/8/7K b - - 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/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 b - - 0 1
|
||||||
8/8/1B6/7b/7k/8/2B1b3/7K w - - 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/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/2k5/5q2/5n2/8/5K2/8 b - - 0 1
|
||||||
8/8/3K4/3Nn3/3nN3/4k3/8/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
|
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 b - - 0 1
|
||||||
8/8/7k/7p/7P/7K/8/8 w - - 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/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/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 b - - 0 1
|
||||||
8/8/8/8/8/7K/7P/7k w - - 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/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 b - - 0 1
|
||||||
8/8/k7/p7/P7/K7/8/8 w - - 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/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 b - - 0 1
|
||||||
8/PPPk4/8/8/8/8/4Kppp/8 w - - 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/4k3/8/b6B w - - 0 1
|
||||||
B6b/8/8/8/2K5/5k2/8/b6B b - - 0 1
|
B6b/8/8/8/2K5/5k2/8/b6B b - - 0 1
|
||||||
K1k5/8/P7/8/8/8/8/8 w - - 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 b - - 0 1
|
||||||
k7/6p1/8/8/8/8/7P/K7 w - - 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 b - - 0 1
|
||||||
k7/7p/8/8/8/8/6P1/K7 w - - 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 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/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 b - - 0 1
|
||||||
k7/8/3p4/8/3P4/8/8/7K w - - 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 b - - 0 1
|
||||||
k7/8/3p4/8/8/4P3/8/7K w - - 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 b - - 0 1
|
||||||
k7/8/6p1/8/8/7P/8/K7 w - - 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 b - - 0 1
|
||||||
k7/8/7p/8/8/6P1/8/K7 w - - 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 b - - 0 1
|
||||||
k7/8/8/3p4/4p3/8/8/7K w - - 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 b - - 0 1
|
||||||
k7/8/8/6p1/7P/8/8/K7 w - - 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 b - - 0 1
|
||||||
k7/8/8/7p/6P1/8/8/K7 w - - 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 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/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 b - - 0 1
|
||||||
n1n5/1Pk5/8/8/8/8/5Kp1/5N1N w - - 0 1
|
n1n5/1Pk5/8/8/8/8/5Kp1/5N1N w - - 0 1
|
||||||
n1n5/PPPk4/8/8/8/8/4Kppp/5N1N b - - 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
|
r3k3/8/8/8/8/8/8/4K3 w q - 0 1
|
||||||
r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10
|
r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10
|
||||||
r6r/1b2k1bq/8/8/7B/8/8/R3K2R b KQ - 3 2
|
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
|
rnb2k1r/pp1Pbppp/2p5/q7/2B5/8/PPPQNnPP/RNB1K2R w KQ - 3 9
|
||||||
rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8
|
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
|
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
|
- 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
|
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.
|
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