Compare commits

...

35 Commits

Author SHA1 Message Date
Mattia Giambirtone 23d7a0427f Merge branch 'master' into performance 2024-04-23 18:58:54 +02:00
Mattia Giambirtone d03b2c2fbf Final bug fixes. Test suite is passing 2024-04-23 18:57:38 +02:00
Mattia Giambirtone e62c78e4cc More movegen bug fixes and minor improvements to the test suite 2024-04-23 11:48:11 +02:00
Mattia Giambirtone 0dfd647f4c Make test suite optionally parallel. Many bug fixes 2024-04-23 01:50:56 +02:00
Mattia Giambirtone 04bfe74ad5 Add nim.cfg and various bugfixes to movegen 2024-04-21 15:58:31 +02:00
Mattia Giambirtone 68c170568e Add missing license headers 2024-04-21 11:11:28 +02:00
Mattia Giambirtone 1d6c74611b Add missing license headers 2024-04-21 11:09:12 +02:00
Mattia Giambirtone 4404ce10b9 Further improve modularity 2024-04-21 11:07:15 +02:00
Mattia Giambirtone c072576b23 Refactor package structure 2024-04-21 10:51:11 +02:00
Mattia Giambirtone fe987576c3 Bug fixes to checks, pins and more. Reworking pawn movegen 2024-04-20 23:47:57 +02:00
Mattia Giambirtone d5bcd15c48 Bug fixes and huge performance improvement. Initial work on pins 2024-04-20 17:48:18 +02:00
Mattia Giambirtone 4a9deb517a Refactoring, cleanup, improve modularity 2024-04-20 14:52:45 +02:00
Mattia Giambirtone f5135ef69e Refactoring, cleanup, improve modularity 2024-04-20 14:51:50 +02:00
Mattia Giambirtone 9528fb9849 Fix crashes 2024-04-20 13:33:42 +02:00
Mattia Giambirtone 77ff697df7 Make position bitboard management more idiomatic 2024-04-20 13:28:14 +02:00
Mattia Giambirtone 2b16b5ec61 More work on attack handling and some bug fixes 2024-04-19 23:28:46 +02:00
Mattia Giambirtone 6fbcd4ff74 Speedups 2024-04-19 21:43:56 +02:00
Mattia Giambirtone 64c30b8a90 More bug fixes. Still borked. Improve modularity 2024-04-19 21:00:52 +02:00
Mattia Giambirtone 0496047164 Fix bugs in move handling 2024-04-19 17:05:22 +02:00
Mattia Giambirtone fcbe15f275 Fixed package errors and embedded magics into the build 2024-04-19 15:50:51 +02:00
Mattia Giambirtone 19ad46bbda Add move generation for bishops and queens as well as attack tracking 2024-04-19 14:38:35 +02:00
Mattia Giambirtone 82cef11cc4 Refactor directory structure. Fix magic bitboard generation and add utilities to dump them to disk 2024-04-19 13:40:58 +02:00
Mattia Giambirtone 6a548bf372 Magic bitboards can now be found (untested) 2024-04-19 00:03:19 +02:00
Mattia Giambirtone 244ad1725a Work on magic bitboard generation 2024-04-18 21:17:29 +02:00
Mattia Giambirtone a07e9cc475 Added knight movegen. Updated attack tracking. Fix bugs 2024-04-17 20:27:39 +02:00
Mattia Giambirtone 3bb2cc7c66 Add king move generation 2024-04-17 16:50:55 +02:00
Mattia Giambirtone 86265c68f0 Fix bugs with pawn movegen and add promotions 2024-04-17 11:54:45 +02:00
Mattia Giambirtone e50cfb9d64 Refactoring of Square handling, removed old code. Initial move generation work on pawns 2024-04-16 23:45:32 +02:00
Mattia Giambirtone 3299f09e1f Add MoveList implementation 2024-04-16 16:29:21 +02:00
Mattia Giambirtone b5181317ef Improve modularity and add bitboard tests 2024-04-16 15:24:48 +02:00
Mattia Giambirtone e1ccdc728e Drop old attack tracking system in preparation for bitboards 2024-04-16 08:50:42 +02:00
Mattia Giambirtone c9988cd939 Improve bitboard interface 2024-04-15 12:45:47 +02:00
Mattia Giambirtone 6115191ed6 Refactoring and more work on bitboard handling 2024-04-15 12:04:50 +02:00
Mattia Giambirtone 75d93a0d59 Minor changes, add more tests 2024-04-13 21:23:12 +02:00
Mattia Giambirtone 7a885b65a0 Add draw by 50 move rule. Initial work on draw by insufficient material (currently borked) 2024-04-13 19:59:54 +02:00
30 changed files with 3061 additions and 2475 deletions

1
.gitignore vendored
View File

@ -2,7 +2,6 @@
nimcache/
nimblecache/
htmldocs/
nim.cfg
bin
# Python
__pycache__

8
Chess/README.md Normal file
View File

@ -0,0 +1,8 @@
# A chess engine written in nim
For now, that's about it.
# Installation
Just run `nimble install`

5
Chess/nim.cfg Normal file
View File

@ -0,0 +1,5 @@
--cc:clang
-o:"bin/nimfish"
-d:danger
--passL:"-flto"
--passC:"-Ofast -flto -march=native -mtune=native"

20
Chess/nimfish.nimble Normal file
View File

@ -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"

35
Chess/nimfish/nimfish.nim Normal file
View File

@ -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())

View File

@ -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]

View File

@ -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

View File

@ -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"]

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -10,6 +10,8 @@ from argparse import ArgumentParser, Namespace
def main(args: Namespace) -> int:
if args.silent:
print = lambda *_: ...
else:
print = __builtins__.print
print("Nimfish move validator v0.0.1 by nocturn9x")
try:
STOCKFISH = (args.stockfish or Path(which("stockfish"))).resolve(strict=True)
@ -17,7 +19,7 @@ def main(args: Namespace) -> int:
print(f"Could not locate stockfish executable -> {type(e).__name__}: {e}")
return 2
try:
NIMFISH = (args.nimfish or (Path.cwd() / "bin" / "nimfish")).resolve(strict=True)
NIMFISH = (args.nimfish or Path(which("nimfish"))).resolve(strict=True)
except Exception as e:
print(f"Could not locate nimfish executable -> {type(e).__name__}: {e}")
return 2
@ -164,4 +166,7 @@ if __name__ == "__main__":
parser.add_argument("--stockfish", type=Path, help="Path to the stockfish executable. Defaults to '' (detected automatically)", default=None)
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None)
parser.add_argument("--silent", action="store_true", help="Disable all output (a return code of 0 means the test was successful)", default=False)
sys.exit(main(parser.parse_args()))
try:
sys.exit(main(parser.parse_args()))
except KeyboardInterrupt:
sys.exit(255)

View File

@ -1,3 +1,6 @@
1B6/8/8/8/5pP1/8/7k/4K3 b - g3 0 1
1k6/8/8/8/4Pp2/8/7B/4K3 b - e3 0 1
1k6/8/8/8/5pP1/8/7B/4K3 b - g3 0 1
1r2k2r/8/8/8/8/8/8/R3K2R b KQk - 0 1
1r2k2r/8/8/8/8/8/8/R3K2R w KQk - 0 1
2K2r2/4P3/8/8/8/8/8/3k4 w - - 0 1
@ -14,21 +17,56 @@
4k2r/8/8/8/8/8/8/4K3 b k - 0 1
4k2r/8/8/8/8/8/8/4K3 w k - 0 1
4k3/1P6/8/8/8/8/K7/8 w - - 0 1
4k3/2rn4/8/2K1pP2/8/8/8/8 w - e6 0 1
4k3/4p3/4K3/8/8/8/8/8 b - - 0 1
4k3/7K/8/5Pp1/8/8/8/1b6 w - g6 0 1
4k3/7b/8/4pP2/4K3/8/8/8 w - e6 0 1
4k3/7b/8/4pP2/8/8/8/1K6 w - e6 0 1
4k3/7b/8/5Pp1/8/8/8/1K6 w - g6 0 1
4k3/8/1b6/2Pp4/3K4/8/8/8 w - d6 0 1
4k3/8/3K4/1pP5/8/q7/8/8 w - b6 0 1
4k3/8/4q3/8/8/8/3b4/4K3 w - - 0 1
4k3/8/4r3/8/8/8/3p4/4K3 w - - 0 1
4k3/8/6b1/4pP2/4K3/8/8/8 w - e6 0 1
4k3/8/7b/5pP1/5K2/8/8/8 w - f6 0 1
4k3/8/8/2PpP3/8/8/8/4K3 w - d6 0 1
4k3/8/8/4pP2/3K4/8/8/8 w - e6 0 1
4k3/8/8/8/1b2r3/8/3Q4/4K3 w - - 0 1
4k3/8/8/8/1b2r3/8/3QP3/4K3 w - - 0 1
4k3/8/8/8/1b5b/2Q5/5P2/4K3 w - - 0 1
4k3/8/8/8/1b5b/2R5/5P2/4K3 w - - 0 1
4k3/8/8/8/1b5b/8/3Q4/4K3 w - - 0 1
4k3/8/8/8/1b5b/8/3R4/4K3 w - - 0 1
4k3/8/8/8/2pPp3/8/8/4K3 b - d3 0 1
4k3/8/8/8/8/8/8/4K2R b K - 0 1
4k3/8/8/8/8/8/8/4K2R w K - 0 1
4k3/8/8/8/8/8/8/R3K2R b KQ - 0 1
4k3/8/8/8/8/8/8/R3K2R w KQ - 0 1
4k3/8/8/8/8/8/8/R3K3 b Q - 0 1
4k3/8/8/8/8/8/8/R3K3 w Q - 0 1
4k3/8/8/K2pP2q/8/8/8/8 w - d6 0 1
4k3/8/8/K2pP2r/8/8/8/8 w - d6 0 1
4k3/8/8/q2pP2K/8/8/8/8 w - d6 0 1
4k3/8/8/r2pP2K/8/8/8/8 w - d6 0 1
4k3/8/K6q/3pP3/8/8/8/8 w - d6 0 1
4k3/8/K6r/3pP3/8/8/8/8 w - d6 0 1
4k3/8/b7/1Pp5/2K5/8/8/8 w - c6 0 1
4k3/K7/8/1pP5/8/8/8/6b1 w - b6 0 1
4k3/b7/8/1pP5/8/8/8/6K1 w - b6 0 1
4k3/b7/8/2Pp4/3K4/8/8/8 w - d6 0 1
4k3/b7/8/2Pp4/8/8/8/6K1 w - d6 0 1
5k2/8/8/8/8/8/8/4K2R w K - 0 1
6B1/8/8/8/1Pp5/8/k7/4K3 b - b3 0 1
6KQ/8/8/8/8/8/8/7k b - - 0 1
6k1/8/8/8/1Pp5/8/B7/4K3 b - b3 0 1
6k1/8/8/8/2pP4/8/B7/3K4 b - d3 0 1
6kq/8/8/8/8/8/8/7K w - - 0 1
6qk/8/8/8/8/8/8/7K b - - 0 1
7k/3p4/8/8/3P4/8/8/K7 b - - 0 1
7k/3p4/8/8/3P4/8/8/K7 w - - 0 1
7K/7p/7k/8/8/8/8/8 b - - 0 1
7K/7p/7k/8/8/8/8/8 w - - 0 1
7k/3p4/8/8/3P4/8/8/K7 b - - 0 1
7k/3p4/8/8/3P4/8/8/K7 w - - 0 1
7k/4K3/8/1pP5/8/q7/8/8 w - b6 0 1
7k/8/1p6/8/8/P7/8/7K b - - 0 1
7k/8/1p6/8/8/P7/8/7K w - - 0 1
7k/8/8/1p6/P7/8/8/7K b - - 0 1
@ -52,8 +90,8 @@
8/3k4/3p4/8/3P4/3K4/8/8 w - - 0 1
8/8/1B6/7b/7k/8/2B1b3/7K b - - 0 1
8/8/1B6/7b/7k/8/2B1b3/7K w - - 0 1
8/8/1k6/2b5/2pP4/8/5K2/8 b - d3 0 1
8/8/1P2K3/8/2n5/1q6/8/5k2 b - - 0 1
8/8/1k6/2b5/2pP4/8/5K2/8 b - d3 0 1
8/8/2k5/5q2/5n2/8/5K2/8 b - - 0 1
8/8/3K4/3Nn3/3nN3/4k3/8/8 b - - 0 1
8/8/3k4/3p4/3P4/3K4/8/8 b - - 0 1
@ -65,6 +103,11 @@
8/8/7k/7p/7P/7K/8/8 b - - 0 1
8/8/7k/7p/7P/7K/8/8 w - - 0 1
8/8/8/2k5/2pP4/8/B7/4K3 b - d3 0 3
8/8/8/4k3/5Pp1/8/8/3K4 b - f3 0 1
8/8/8/8/1R1Pp2k/8/8/4K3 b - d3 0 1
8/8/8/8/1k1Pp2R/8/8/4K3 b - d3 0 1
8/8/8/8/1k1PpN1R/8/8/4K3 b - d3 0 1
8/8/8/8/1k1Ppn1R/8/8/4K3 b - d3 0 1
8/8/8/8/8/4k3/4P3/4K3 w - - 0 1
8/8/8/8/8/7K/7P/7k b - - 0 1
8/8/8/8/8/7K/7P/7k w - - 0 1
@ -76,45 +119,49 @@
8/8/8/8/8/K7/P7/k7 w - - 0 1
8/8/k7/p7/P7/K7/8/8 b - - 0 1
8/8/k7/p7/P7/K7/8/8 w - - 0 1
8/k1P5/8/1K6/8/8/8/8 w - - 0 1
8/P1k5/K7/8/8/8/8/8 w - - 0 1
8/Pk6/8/8/8/8/6Kp/8 b - - 0 1
8/Pk6/8/8/8/8/6Kp/8 w - - 0 1
8/PPPk4/8/8/8/8/4Kppp/8 b - - 0 1
8/PPPk4/8/8/8/8/4Kppp/8 w - - 0 1
8/Pk6/8/8/8/8/6Kp/8 b - - 0 1
8/Pk6/8/8/8/8/6Kp/8 w - - 0 1
8/k1P5/8/1K6/8/8/8/8 w - - 0 1
B6b/8/8/8/2K5/4k3/8/b6B w - - 0 1
B6b/8/8/8/2K5/5k2/8/b6B b - - 0 1
K1k5/8/P7/8/8/8/8/8 w - - 0 1
K7/8/2n5/1n6/8/8/8/k6N b - - 0 1
K7/8/2n5/1n6/8/8/8/k6N w - - 0 1
K7/8/8/3Q4/4q3/8/8/7k b - - 0 1
K7/8/8/3Q4/4q3/8/8/7k w - - 0 1
K7/b7/1b6/1b6/8/8/8/k6B b - - 0 1
K7/b7/1b6/1b6/8/8/8/k6B w - - 0 1
K7/p7/k7/8/8/8/8/8 b - - 0 1
K7/p7/k7/8/8/8/8/8 w - - 0 1
R6r/8/8/2K5/5k2/8/8/r6R b - - 0 1
R6r/8/8/2K5/5k2/8/8/r6R w - - 0 1
k3K3/8/8/3pP3/8/8/8/4r3 w - d6 0 1
k7/6p1/8/8/8/8/7P/K7 b - - 0 1
k7/6p1/8/8/8/8/7P/K7 w - - 0 1
k7/7p/8/8/8/8/6P1/K7 b - - 0 1
k7/7p/8/8/8/8/6P1/K7 w - - 0 1
K7/8/2n5/1n6/8/8/8/k6N b - - 0 1
k7/8/2N5/1N6/8/8/8/K6n b - - 0 1
K7/8/2n5/1n6/8/8/8/k6N w - - 0 1
k7/8/2N5/1N6/8/8/8/K6n w - - 0 1
k7/8/3p4/8/3P4/8/8/7K b - - 0 1
k7/8/3p4/8/3P4/8/8/7K w - - 0 1
k7/8/3p4/8/8/4P3/8/7K b - - 0 1
k7/8/3p4/8/8/4P3/8/7K w - - 0 1
k7/8/4r3/3pP3/8/8/8/4K3 w - d6 0 1
k7/8/6p1/8/8/7P/8/K7 b - - 0 1
k7/8/6p1/8/8/7P/8/K7 w - - 0 1
k7/8/7p/8/8/6P1/8/K7 b - - 0 1
k7/8/7p/8/8/6P1/8/K7 w - - 0 1
k7/8/8/3p4/4p3/8/8/7K b - - 0 1
k7/8/8/3p4/4p3/8/8/7K w - - 0 1
K7/8/8/3Q4/4q3/8/8/7k b - - 0 1
K7/8/8/3Q4/4q3/8/8/7k w - - 0 1
k7/8/8/6p1/7P/8/8/K7 b - - 0 1
k7/8/8/6p1/7P/8/8/K7 w - - 0 1
k7/8/8/7p/6P1/8/8/K7 b - - 0 1
k7/8/8/7p/6P1/8/8/K7 w - - 0 1
k7/B7/1B6/1B6/8/8/8/K6b b - - 0 1
K7/b7/1b6/1b6/8/8/8/k6B b - - 0 1
k7/B7/1B6/1B6/8/8/8/K6b w - - 0 1
K7/b7/1b6/1b6/8/8/8/k6B w - - 0 1
K7/p7/k7/8/8/8/8/8 b - - 0 1
K7/p7/k7/8/8/8/8/8 w - - 0 1
n1n5/1Pk5/8/8/8/8/5Kp1/5N1N b - - 0 1
n1n5/1Pk5/8/8/8/8/5Kp1/5N1N w - - 0 1
n1n5/PPPk4/8/8/8/8/4Kppp/5N1N b - - 0 1
@ -142,8 +189,6 @@ r3k3/8/8/8/8/8/8/4K3 b q - 0 1
r3k3/8/8/8/8/8/8/4K3 w q - 0 1
r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10
r6r/1b2k1bq/8/8/7B/8/8/R3K2R b KQ - 3 2
R6r/8/8/2K5/5k2/8/8/r6R b - - 0 1
R6r/8/8/2K5/5k2/8/8/r6R w - - 0 1
rnb2k1r/pp1Pbppp/2p5/q7/2B5/8/PPPQNnPP/RNB1K2R w KQ - 3 9
rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8
rnbqkb1r/ppppp1pp/7n/4Pp2/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3

84
Chess/tests/suite.py Normal file
View File

@ -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)

View File

@ -12,6 +12,6 @@ in semiconductor technology smart enough to play tic tac toe.
- Chess -> WIP
All of these games will be played using decision trees searched using the minimax algorithm (maybe a bit of neural networks too, who knows).
All of these games will be played using decision trees searched using the minimax algorithm or variations thereof (maybe a bit of neural networks too, who knows).
Ideally I'd like to implement a bunch of stuff such as move reordering, alpha-beta pruning and transpositions in order to improve both
processing time and decision quality. Very much WIP.

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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()))