Refactoring and bug fixes
This commit is contained in:
parent
a4954a971b
commit
77129855df
|
@ -14,7 +14,8 @@
|
|||
|
||||
import std/strutils
|
||||
import std/strformat
|
||||
import std/sequtils
|
||||
import std/times
|
||||
import std/math
|
||||
|
||||
|
||||
type
|
||||
|
@ -75,7 +76,6 @@ type
|
|||
|
||||
Position* = ref object
|
||||
## A chess position
|
||||
move: Move
|
||||
# Did the rooks on either side/the king move?
|
||||
castlingAvailable: tuple[white, black: tuple[queen, king: bool]]
|
||||
# Number of half-moves that were performed
|
||||
|
@ -103,12 +103,12 @@ type
|
|||
ChessBoard* = ref object
|
||||
## A chess board object
|
||||
|
||||
# An 8x8 matrix we use for constant
|
||||
# time lookup of pieces by their location
|
||||
# The actual board where pieces live
|
||||
# (flattened 8x8 matrix)
|
||||
grid: seq[Piece]
|
||||
# The current position
|
||||
position: Position
|
||||
# List of all reached positions
|
||||
# List of all previously reached positions
|
||||
positions: seq[Position]
|
||||
|
||||
|
||||
|
@ -137,12 +137,15 @@ proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location]
|
|||
proc getAttacks*(self: ChessBoard, loc: Location): Attacked
|
||||
proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked]
|
||||
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool
|
||||
proc toFEN*(self: ChessBoard): string
|
||||
proc undoLastMove*(self: ChessBoard)
|
||||
|
||||
|
||||
proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} =
|
||||
for x in other:
|
||||
self.add(x)
|
||||
|
||||
proc resetBoard*(self: ChessBoard)
|
||||
|
||||
# Due to our board layout, directions of movement are reversed for white and black, so
|
||||
# we need these helpers to avoid going mad with integer tuples and minus signs everywhere
|
||||
|
@ -260,6 +263,18 @@ func getStartRow(piece: Piece): int {.inline.} =
|
|||
return 0
|
||||
|
||||
|
||||
func getKingStartingPosition(color: PieceColor): Location {.inline.} =
|
||||
## Retrieves the starting location of the king
|
||||
## for the given color
|
||||
case color:
|
||||
of White:
|
||||
return (7, 4)
|
||||
of Black:
|
||||
return (0, 4)
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
func getLastRow(color: PieceColor): int {.inline.} =
|
||||
## Retrieves the location of the last
|
||||
## row relative to the given color
|
||||
|
@ -277,9 +292,10 @@ proc newChessboard: ChessBoard =
|
|||
new(result)
|
||||
# Turns our flat sequence into an 8x8 grid
|
||||
result.grid = newSeqOfCap[Piece](64)
|
||||
for _ in 0..63:
|
||||
result.grid.add(emptyPiece())
|
||||
result.position = Position(attacked: (@[], @[]),
|
||||
enPassantSquare: emptyLocation(),
|
||||
move: emptyMove(),
|
||||
turn: White,
|
||||
fullMoveCount: 1,
|
||||
pieces: (white: (king: emptyLocation(),
|
||||
|
@ -331,10 +347,8 @@ proc newChessboardFromFEN*(fen: string): ChessBoard =
|
|||
of 'r', 'n', 'b', 'q', 'k', 'p':
|
||||
# We know for a fact these values are in our
|
||||
# enumeration, so all is good
|
||||
{.push.}
|
||||
{.warning[HoleEnumConv]:off.}
|
||||
piece = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black)
|
||||
{.pop.}
|
||||
case piece.color:
|
||||
of Black:
|
||||
case piece.kind:
|
||||
|
@ -462,8 +476,8 @@ proc newDefaultChessboard*: ChessBoard {.inline.} =
|
|||
|
||||
proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int =
|
||||
## Returns the number of pieces with
|
||||
## the given color and type in the given
|
||||
## position
|
||||
## the given color and type in the
|
||||
## current position
|
||||
case color:
|
||||
of White:
|
||||
case kind:
|
||||
|
@ -519,9 +533,9 @@ func rankToColumn(rank: int): int8 {.inline.} =
|
|||
return indeces[rank - 1]
|
||||
|
||||
|
||||
func rowToRank(row: int): int {.inline.} =
|
||||
func rowToFile(row: int): int {.inline.} =
|
||||
## Converts a row into our grid into
|
||||
## a chess rank
|
||||
## a chess file
|
||||
const indeces = [8, 7, 6, 5, 4, 3, 2, 1]
|
||||
return indeces[row]
|
||||
|
||||
|
@ -548,7 +562,7 @@ proc algebraicToLocation*(s: string): Location =
|
|||
func locationToAlgebraic*(loc: Location): string {.inline.} =
|
||||
## Converts a location from our internal row, column
|
||||
## notation to a square in algebraic notation
|
||||
return &"{char(uint8(loc.col) + uint8('a'))}{rowToRank(loc.row)}"
|
||||
return &"{char(uint8(loc.col) + uint8('a'))}{rowToFile(loc.row)}"
|
||||
|
||||
|
||||
func getPiece*(self: ChessBoard, loc: Location): Piece {.inline.} =
|
||||
|
@ -582,7 +596,7 @@ func getPromotionType*(move: Move): MoveFlag {.inline.} =
|
|||
func isCapture*(move: Move): bool {.inline.} =
|
||||
## Returns whether the given move is a
|
||||
## cature
|
||||
result = (move.flags and Capture.uint16) != 0
|
||||
result = (move.flags and Capture.uint16) == Capture.uint16
|
||||
|
||||
|
||||
func isCastling*(move: Move): bool {.inline.} =
|
||||
|
@ -614,6 +628,30 @@ func isDoublePush*(move: Move): bool {.inline.} =
|
|||
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, CastleLong, CastleShort,
|
||||
PromoteToBishop, PromoteToKnight, PromoteToQueen,
|
||||
PromoteToRook]:
|
||||
if (move.flags and flag.uint16) == flag.uint16:
|
||||
result.add(flag)
|
||||
if result.len() == 0:
|
||||
result.add(Default)
|
||||
|
||||
|
||||
func getKing(self: ChessBoard, color: PieceColor): Location {.inline.} =
|
||||
var color = color
|
||||
if color == None:
|
||||
color = self.getActiveColor()
|
||||
case color:
|
||||
of White:
|
||||
return self.position.pieces.white.king
|
||||
of Black:
|
||||
return self.position.pieces.black.king
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
|
||||
## Returns whether the given color's
|
||||
## king is in check. If the color is
|
||||
|
@ -650,6 +688,12 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king:
|
|||
of None:
|
||||
# Unreachable
|
||||
discard
|
||||
# Some of these checks may seem redundant, but we
|
||||
# perform them because they're less expensive
|
||||
|
||||
# King is not on its starting square
|
||||
if self.getKing(color) != getKingStartingPosition(color):
|
||||
return (false, false)
|
||||
if self.inCheck(color):
|
||||
# King can not castle out of check
|
||||
return (false, false)
|
||||
|
@ -752,6 +796,7 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] =
|
|||
# in which case either the king has to move or
|
||||
# that piece has to be captured, but this is
|
||||
# already implicitly handled by the loop below)
|
||||
|
||||
var location = attacker
|
||||
while location != king:
|
||||
location = location + attack.direction
|
||||
|
@ -760,40 +805,24 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] =
|
|||
result.add(location)
|
||||
|
||||
|
||||
func getKing(self: ChessBoard, color: PieceColor): Location {.inline.} =
|
||||
var color = color
|
||||
if color == None:
|
||||
color = self.getActiveColor()
|
||||
case color:
|
||||
of White:
|
||||
return self.position.pieces.white.king
|
||||
of Black:
|
||||
return self.position.pieces.black.king
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
|
||||
## Generates the possible moves for the pawn in the given
|
||||
## location
|
||||
var
|
||||
piece = self.grid[location.row, location.col]
|
||||
locations: seq[Location] = @[]
|
||||
flags: seq[MoveFlag] = @[]
|
||||
targets: seq[Location] = @[]
|
||||
doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
|
||||
# Pawns can move forward one square
|
||||
let forward = piece.color.topSide() + location
|
||||
# Only if the square is empty though
|
||||
if forward.isValid() and self.grid[forward.row, forward.col].color == None:
|
||||
locations.add(forward)
|
||||
flags.add(Default)
|
||||
targets.add(forward)
|
||||
# If the pawn is on its first rank, it can push two squares
|
||||
if location.row == piece.getStartRow():
|
||||
let double = location + piece.color.doublePush()
|
||||
# Check that both squares are empty
|
||||
if double.isValid() and self.grid[forward.row, forward.col].color == None and self.grid[double.row, double.col].color == None:
|
||||
locations.add(double)
|
||||
flags.add(DoublePush)
|
||||
targets.add(double)
|
||||
let enPassantPawn = self.getEnPassantTarget() + piece.color.opposite().topSide()
|
||||
# They can also move on either diagonal one
|
||||
# square, but only to capture or for en passant
|
||||
|
@ -819,40 +848,37 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
|
|||
if p.color == piece.color.opposite() and p.kind in [Queen, Rook]:
|
||||
ok = false
|
||||
if ok:
|
||||
locations.add(diagonal)
|
||||
flags.add(EnPassant)
|
||||
targets.add(diagonal)
|
||||
elif otherPiece.color == piece.color.opposite() and otherPiece.kind != King:
|
||||
locations.add(diagonal)
|
||||
flags.add(Capture)
|
||||
var
|
||||
newLocation: Location
|
||||
newFlags: seq[MoveFlag]
|
||||
newLocations: seq[Location]
|
||||
targets.add(diagonal)
|
||||
# Check for pins
|
||||
let pins = self.getPinnedDirections(location)
|
||||
for pin in pins:
|
||||
newLocation = location + pin
|
||||
let loc = locations.find(newLocation)
|
||||
if loc != -1:
|
||||
# Pin direction is legal for this piece
|
||||
newLocations.add(newLocation)
|
||||
newFlags.add(flags[loc])
|
||||
if pins.len() > 0:
|
||||
locations = newLocations
|
||||
flags = newFlags
|
||||
let pinned = self.getPinnedDirections(location)
|
||||
if pinned.len() > 0:
|
||||
var newTargets: seq[Location] = @[]
|
||||
for target in targets:
|
||||
if target in pinned:
|
||||
newTargets.add(target)
|
||||
targets = newTargets
|
||||
let checked = self.inCheck()
|
||||
let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
|
||||
var targetPiece: Piece
|
||||
for (target, flag) in zip(locations, flags):
|
||||
for target in targets:
|
||||
if checked and target notin resolutions:
|
||||
continue
|
||||
targetPiece = self.grid[target.row, target.col]
|
||||
var flags: uint16 = Default.uint16
|
||||
if targetPiece.color != None:
|
||||
flags = flags or Capture.uint16
|
||||
elif abs(location.row - target.row) == 2:
|
||||
flags = flags or DoublePush.uint16
|
||||
elif target == self.getEnPassantTarget():
|
||||
flags = flags or EnPassant.uint16
|
||||
if target.row == piece.color.getLastRow():
|
||||
# Pawn reached the other side of the board: generate all potential piece promotions
|
||||
for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]:
|
||||
result.add(Move(startSquare: location, targetSquare: target, flags: promotionType.uint16 or flag.uint16))
|
||||
result.add(Move(startSquare: location, targetSquare: target, flags: promotionType.uint16 or flags))
|
||||
continue
|
||||
result.add(Move(startSquare: location, targetSquare: target, flags: flag.uint16))
|
||||
result.add(Move(startSquare: location, targetSquare: target, flags: flags))
|
||||
|
||||
|
||||
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
|
||||
|
@ -874,7 +900,11 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
|
|||
directions.add(piece.color.leftSide())
|
||||
let pinned = self.getPinnedDirections(location)
|
||||
if pinned.len() > 0:
|
||||
directions = pinned
|
||||
var newDirections: seq[Location] = @[]
|
||||
for direction in directions:
|
||||
if direction in pinned:
|
||||
newDirections.add(direction)
|
||||
directions = newDirections
|
||||
let checked = self.inCheck()
|
||||
let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
|
||||
for direction in directions:
|
||||
|
@ -892,7 +922,9 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
|
|||
if otherPiece.color == piece.color:
|
||||
break
|
||||
if checked and square notin resolutions:
|
||||
break
|
||||
# We don't break out of the loop because
|
||||
# we might resolve the check later
|
||||
continue
|
||||
if otherPiece.color == piece.color.opposite:
|
||||
# Target square contains an enemy piece: capture
|
||||
# it and stop going any further
|
||||
|
@ -978,7 +1010,6 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
|
|||
# A friendly piece or the opponent king is is in the way
|
||||
if otherPiece.color == piece.color or otherPiece.kind == King:
|
||||
continue
|
||||
|
||||
if checked and square notin resolutions:
|
||||
continue
|
||||
if otherPiece.color != None:
|
||||
|
@ -1070,7 +1101,7 @@ proc getAttacks*(self: ChessBoard, loc: Location): Attacked =
|
|||
|
||||
|
||||
proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location] =
|
||||
## Returns the first attacks of the piece in the given
|
||||
## Returns the first attack of the piece in the given
|
||||
## source location that also attacks the target location
|
||||
let piece = self.grid[source.row, source.col]
|
||||
case piece.color:
|
||||
|
@ -1283,11 +1314,12 @@ proc updateAttackedSquares(self: ChessBoard) =
|
|||
self.updateKingAttacks()
|
||||
|
||||
|
||||
proc removePiece(self: ChessBoard, location: Location, attack: bool = true) =
|
||||
proc removePiece(self: ChessBoard, location: Location, attack: bool = true, empty: bool = true) =
|
||||
## Removes a piece from the board, updating necessary
|
||||
## metadata
|
||||
var piece = self.grid[location.row, location.col]
|
||||
self.grid[location.row, location.col] = emptyPiece()
|
||||
if empty:
|
||||
self.grid[location.row, location.col] = emptyPiece()
|
||||
case piece.color:
|
||||
of White:
|
||||
case piece.kind:
|
||||
|
@ -1388,11 +1420,11 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
|
|||
self.updateAttackedSquares()
|
||||
|
||||
|
||||
|
||||
proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bool = true) =
|
||||
## Like the other movePiece(), but with two locations
|
||||
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack)
|
||||
|
||||
|
||||
proc doMove(self: ChessBoard, move: Move) =
|
||||
## Internal function called by makeMove after
|
||||
## performing legality checks. Can be used in
|
||||
|
@ -1482,13 +1514,13 @@ proc doMove(self: ChessBoard, move: Move) =
|
|||
castlingAvailable.black.queen = false
|
||||
else:
|
||||
discard
|
||||
|
||||
# Create new position
|
||||
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
|
||||
halfMoveClock: halfMoveClock,
|
||||
fullMoveCount: fullMoveCount,
|
||||
turn: self.getActiveColor().opposite,
|
||||
castlingAvailable: castlingAvailable,
|
||||
move: move,
|
||||
pieces: self.position.pieces,
|
||||
enPassantSquare: enPassantTarget
|
||||
)
|
||||
|
@ -1500,26 +1532,23 @@ proc doMove(self: ChessBoard, move: Move) =
|
|||
var
|
||||
location: Location
|
||||
target: Location
|
||||
flags: uint16
|
||||
if move.getCastlingType() == CastleShort:
|
||||
location = piece.color.kingSideRook()
|
||||
target = shortCastleRook()
|
||||
flags = flags or CastleShort.uint16
|
||||
else:
|
||||
location = piece.color.queenSideRook()
|
||||
target = longCastleRook()
|
||||
flags = flags or CastleLong.uint16
|
||||
let rook = self.grid[location.row, location.col]
|
||||
let move = Move(startSquare: location, targetSquare: location + target, flags: move.flags)
|
||||
let move = Move(startSquare: location, targetSquare: location + target, flags: flags)
|
||||
self.movePiece(move, attack=false)
|
||||
|
||||
if move.isCapture():
|
||||
# Get rid of captured pieces
|
||||
self.removePiece(move.targetSquare, attack=false)
|
||||
|
||||
if move.isEnPassant():
|
||||
# Make the en passant pawn disappear
|
||||
self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false)
|
||||
|
||||
self.movePiece(move, attack=false)
|
||||
|
||||
|
||||
if move.isPromotion():
|
||||
# Move is a pawn promotion: get rid of the pawn
|
||||
# and spawn a new piece
|
||||
|
@ -1535,9 +1564,15 @@ proc doMove(self: ChessBoard, move: Move) =
|
|||
self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color))
|
||||
else:
|
||||
discard
|
||||
# Update attack metadata
|
||||
self.updateAttackedSquares()
|
||||
|
||||
if move.isCapture():
|
||||
# Get rid of captured pieces
|
||||
self.removePiece(move.targetSquare, attack=false, empty=false)
|
||||
# Move the piece to its target square and update attack metadata
|
||||
self.movePiece(move)
|
||||
# TODO: Remove this, once I figure out what the heck is wrong
|
||||
# with updating the board representation
|
||||
self.resetBoard()
|
||||
|
||||
|
||||
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
|
||||
|
@ -1588,9 +1623,7 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
|
|||
proc resetBoard*(self: ChessBoard) =
|
||||
## Resets the internal grid representation
|
||||
## according to the positional data stored
|
||||
## in the chessboard. Warning: this can be
|
||||
## expensive, especially in critical paths
|
||||
## or tight loops
|
||||
## in the chessboard
|
||||
for i in 0..63:
|
||||
self.grid[i] = emptyPiece()
|
||||
for loc in self.position.pieces.white.pawns:
|
||||
|
@ -1618,9 +1651,8 @@ proc resetBoard*(self: ChessBoard) =
|
|||
|
||||
|
||||
proc undoLastMove*(self: ChessBoard) =
|
||||
if self.positions.len() == 0:
|
||||
return
|
||||
self.position = self.positions.pop()
|
||||
if self.positions.len() > 0:
|
||||
self.position = self.positions.pop()
|
||||
self.resetBoard()
|
||||
|
||||
|
||||
|
@ -1778,12 +1810,13 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
|
|||
## the given number of ply
|
||||
|
||||
let moves = self.generateAllMoves()
|
||||
if len(moves) == 0:
|
||||
result.checkmates = 1
|
||||
if ply == 0:
|
||||
result.nodes = 1
|
||||
return
|
||||
if ply == 1 and bulk:
|
||||
if not bulk:
|
||||
if len(moves) == 0 and self.inCheck():
|
||||
result.checkmates = 1
|
||||
if ply == 0:
|
||||
result.nodes = 1
|
||||
return
|
||||
elif ply == 1 and bulk:
|
||||
if divide:
|
||||
var postfix = ""
|
||||
for move in moves:
|
||||
|
@ -1810,7 +1843,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
|
|||
echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}, from ({move.startSquare.row}, {move.startSquare.col}) to ({move.targetSquare.row}, {move.targetSquare.col})"
|
||||
echo &"Turn: {self.getActiveColor()}"
|
||||
echo &"Piece: {self.grid[move.startSquare.row, move.startSquare.col].kind}"
|
||||
echo &"Flag: {move.flags}"
|
||||
echo &"Flags: {move.getFlags()}"
|
||||
echo &"In check: {(if self.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: {self.toFEN()}"
|
||||
|
@ -1821,14 +1854,15 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
|
|||
echo "None"
|
||||
echo "\n", self.pretty()
|
||||
self.doMove(move)
|
||||
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 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 self.inCheck():
|
||||
# Opponent king is in check
|
||||
inc(result.checks)
|
||||
|
@ -1891,7 +1925,7 @@ proc handleGoCommand(board: ChessBoard, command: seq[string]) =
|
|||
var ok = true
|
||||
for arg in args[1..^1]:
|
||||
case arg:
|
||||
of "bulk-count", "bulk":
|
||||
of "bulk":
|
||||
bulk = true
|
||||
of "verbose":
|
||||
verbose = true
|
||||
|
@ -1904,8 +1938,12 @@ proc handleGoCommand(board: ChessBoard, command: seq[string]) =
|
|||
try:
|
||||
let ply = parseInt(args[0])
|
||||
if bulk:
|
||||
echo &"\nNodes searched (bulk-counting: on): {board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes}\n"
|
||||
let t = cpuTime()
|
||||
let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes
|
||||
echo &"\nNodes searched (bulk-counting: on): {nodes}"
|
||||
echo &"Time taken: {round(cpuTime() - t, 3)} seconds\n"
|
||||
else:
|
||||
let t = cpuTime()
|
||||
let data = board.perft(ply, divide=true, verbose=verbose)
|
||||
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
|
||||
echo &" - Captures: {data.captures}"
|
||||
|
@ -1915,40 +1953,14 @@ proc handleGoCommand(board: ChessBoard, command: seq[string]) =
|
|||
echo &" - Castles: {data.castles}"
|
||||
echo &" - Promotions: {data.promotions}"
|
||||
echo ""
|
||||
echo &"Time taken: {round(cpuTime() - t, 3)} seconds"
|
||||
except ValueError:
|
||||
echo "Error: go: perft: invalid depth"
|
||||
else:
|
||||
echo &"Error: go: unknown subcommand '{command[1]}'"
|
||||
|
||||
|
||||
proc handlePositionCommand(board: var ChessBoard, command: seq[string]) =
|
||||
case len(command):
|
||||
of 2:
|
||||
case command[1]:
|
||||
of "startpos":
|
||||
board = newDefaultChessboard()
|
||||
of "current", "cur":
|
||||
echo &"Current position: {board.toFEN()}"
|
||||
of "pretty":
|
||||
echo board.pretty()
|
||||
of "print", "show":
|
||||
echo board
|
||||
else:
|
||||
echo &"Error: position: invalid argument '{command[1]}'"
|
||||
of 3:
|
||||
case command[1]:
|
||||
of "fen":
|
||||
try:
|
||||
board = newChessboardFromFEN(command[2])
|
||||
except ValueError:
|
||||
echo &"Error: position: invalid FEN string '{command[2]}': {getCurrentExceptionMsg()}"
|
||||
else:
|
||||
echo &"Error: position: unknown subcommand '{command[1]}'"
|
||||
else:
|
||||
echo &"Error: position: invalid number of arguments"
|
||||
|
||||
|
||||
proc handleMoveCommand(board: ChessBoard, command: seq[string]) =
|
||||
proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} =
|
||||
if len(command) != 2:
|
||||
echo &"Error: move: invalid number of arguments"
|
||||
return
|
||||
|
@ -1964,18 +1976,22 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]) =
|
|||
try:
|
||||
startSquare = moveString[0..1].algebraicToLocation()
|
||||
except ValueError:
|
||||
echo &"Error: move: invalid start square"
|
||||
echo &"Error: move: invalid start square ({moveString[0..1]})"
|
||||
return
|
||||
try:
|
||||
targetSquare = moveString[2..3].algebraicToLocation()
|
||||
except ValueError:
|
||||
echo &"Error: move: invalid target square"
|
||||
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, castling, etc.)
|
||||
|
||||
if board.grid[targetSquare.row, targetSquare.col].kind != Empty:
|
||||
flags = flags or Capture.uint16
|
||||
|
||||
if board.grid[startSquare.row, startSquare.col].kind == Pawn and abs(startSquare.row - targetSquare.row) == 2:
|
||||
elif board.grid[startSquare.row, startSquare.col].kind == Pawn and abs(startSquare.row - targetSquare.row) == 2:
|
||||
flags = flags or DoublePush.uint16
|
||||
|
||||
if len(moveString) == 5:
|
||||
|
@ -1993,10 +2009,115 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]) =
|
|||
echo &"Error: move: invalid promotion type"
|
||||
return
|
||||
|
||||
let move = Move(startSquare: startSquare, targetSquare: targetSquare, flags: flags)
|
||||
if board.makeMove(move) == emptyMove():
|
||||
echo "Error: move: illegal move"
|
||||
|
||||
var move = Move(startSquare: startSquare, targetSquare: targetSquare, flags: flags)
|
||||
if board.getPiece(move.startSquare).kind == King and move.startSquare == board.getActiveColor().getKingStartingPosition():
|
||||
if move.targetSquare == move.startSquare + longCastleKing():
|
||||
move.flags = move.flags or CastleLong.uint16
|
||||
elif move.targetSquare == move.startSquare + shortCastleKing():
|
||||
move.flags = move.flags or CastleShort.uint16
|
||||
result = board.makeMove(move)
|
||||
if result == emptyMove():
|
||||
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 = newChessboard()
|
||||
case command[1]:
|
||||
of "startpos":
|
||||
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]]) == emptyMove():
|
||||
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]]) == emptyMove():
|
||||
return
|
||||
inc(j)
|
||||
inc(i)
|
||||
board = tempBoard
|
||||
of "print":
|
||||
echo board
|
||||
of "pretty":
|
||||
echo board.pretty()
|
||||
|
||||
|
||||
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
|
||||
- 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 "startpos" and "fen" subcommands: 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 each side
|
||||
- check: Print if the current side to move is in check
|
||||
- undo: Undoes the last move that was performed. Can be used in succession
|
||||
- turn: Print which side is to move
|
||||
- ep: Print the current en passant target
|
||||
- pretty: Shorthand for "position pretty"
|
||||
- print: Shorthand for "position print"
|
||||
"""
|
||||
|
||||
|
||||
proc main: int =
|
||||
## Nimfish's control interface
|
||||
|
@ -2018,17 +2139,30 @@ proc main: int =
|
|||
of "clear":
|
||||
echo "\x1Bc"
|
||||
of "help":
|
||||
echo "TODO"
|
||||
echo HELP_TEXT
|
||||
of "go":
|
||||
handleGoCommand(board, cmd)
|
||||
of "position":
|
||||
handlePositionCommand(board, cmd)
|
||||
of "move":
|
||||
handleMoveCommand(board, cmd)
|
||||
of "pretty":
|
||||
echo board.pretty()
|
||||
of "pretty", "print":
|
||||
handlePositionCommand(board, @["position", cmd[0]])
|
||||
of "undo":
|
||||
board.undoLastMove()
|
||||
of "turn":
|
||||
echo &"Active color: {board.getActiveColor()}"
|
||||
of "ep":
|
||||
let target = board.getEnPassantTarget()
|
||||
if target != emptyLocation():
|
||||
echo &"En passant target: {target.locationToAlgebraic()}"
|
||||
else:
|
||||
echo "En passant target: None"
|
||||
of "castle":
|
||||
let canCastle = board.canCastle()
|
||||
echo &"Castling rights for {($board.getActiveColor()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||||
of "check":
|
||||
echo &"{board.getActiveColor()} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
|
||||
else:
|
||||
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
|
||||
except IOError:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import re
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
|
@ -25,14 +26,19 @@ def main(args: Namespace) -> int:
|
|||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
stdin=subprocess.PIPE,
|
||||
encoding="u8"
|
||||
encoding="u8",
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
print(f"Starting Nimfish engine at {NIMFISH.as_posix()!r}")
|
||||
nimfish_process = subprocess.Popen(NIMFISH,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
stdin=subprocess.PIPE,
|
||||
encoding="u8")
|
||||
encoding="u8",
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
print(f"Setting position to {(args.fen if args.fen else 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1')!r}")
|
||||
if args.fen:
|
||||
nimfish_process.stdin.write(f"position fen {args.fen}\n")
|
||||
|
@ -43,13 +49,8 @@ def main(args: Namespace) -> int:
|
|||
print(f"Engines started, beginning search to depth {args.ply}")
|
||||
nimfish_process.stdin.write(f"go perft {args.ply} {'bulk' if args.bulk else ''}\n")
|
||||
stockfish_process.stdin.write(f"go perft {args.ply}\n")
|
||||
print("Search started, waiting for engine completion")
|
||||
start_time = time.time()
|
||||
stockfish_output = stockfish_process.communicate()[0]
|
||||
stockfish_time = time.time() - start_time
|
||||
start_time = time.time()
|
||||
nimfish_output = nimfish_process.communicate()[0]
|
||||
nimfish_time = time.time() - start_time
|
||||
positions = {
|
||||
"all": {},
|
||||
"stockfish": {},
|
||||
|
@ -89,8 +90,8 @@ def main(args: Namespace) -> int:
|
|||
total_nodes = {"stockfish": sum(positions["stockfish"][move] for move in positions["stockfish"]),
|
||||
"nimfish": sum(positions["nimfish"][move] for move in positions["nimfish"])}
|
||||
total_difference = total_nodes["stockfish"] - total_nodes["nimfish"]
|
||||
print(f"Stockfish searched {total_nodes['stockfish']} node{'' if total_nodes['stockfish'] == 1 else 's'} in {stockfish_time:.2f} seconds")
|
||||
print(f"Nimfish searched {total_nodes['nimfish']} node{'' if total_nodes['nimfish'] == 1 else 's'} in {nimfish_time:.2f} seconds")
|
||||
print(f"Stockfish searched {total_nodes['stockfish']} node{'' if total_nodes['stockfish'] == 1 else 's'}")
|
||||
print(f"Nimfish searched {total_nodes['nimfish']} node{'' if total_nodes['nimfish'] == 1 else 's'}")
|
||||
|
||||
if total_difference > 0:
|
||||
print(f"Nimfish searched {total_difference} less node{'' if total_difference == 1 else 's'} than Stockfish")
|
||||
|
@ -117,13 +118,12 @@ def main(args: Namespace) -> int:
|
|||
"To fix this, re-run the program without the --bulk option")
|
||||
if extra:
|
||||
print(f" Breakdown by move type:")
|
||||
print(f" - Captures: {extra.group('captures')}")
|
||||
print(f" - Checks: {extra.group('checks')}")
|
||||
print(f" - En Passant: {extra.group('enPassant')}")
|
||||
print(f" - Checkmates: {extra.group('checkmates')}")
|
||||
print(f" - Castles: {extra.group('castles')}")
|
||||
print(f" - Promotions: {extra.group('promotions')}")
|
||||
print(f" - Total: {total_nodes['nimfish']}")
|
||||
print(f" - Captures: {extra.group('captures')}")
|
||||
print(f" - Checks: {extra.group('checks')}")
|
||||
print(f" - En Passant: {extra.group('enPassant')}")
|
||||
print(f" - Checkmates: {extra.group('checkmates')}")
|
||||
print(f" - Castles: {extra.group('castles')}")
|
||||
print(f" - Promotions: {extra.group('promotions')}")
|
||||
|
||||
elif not args.bulk:
|
||||
print("Unable to locate move breakdown in Nimfish output")
|
||||
|
@ -134,11 +134,11 @@ def main(args: Namespace) -> int:
|
|||
for move in missing["stockfish"]:
|
||||
print(f" - {move}: {positions['stockfish'][move]}")
|
||||
if missing["nimfish"]:
|
||||
print(" Illegal moves generated: ")
|
||||
print("\n Illegal moves generated: ")
|
||||
for move in missing["nimfish"]:
|
||||
print(f" - {move}: {positions['nimfish'][move]}")
|
||||
if mistakes:
|
||||
print(" Counting mistakes made:")
|
||||
print("\n Counting mistakes made:")
|
||||
for move in mistakes:
|
||||
missed = positions["stockfish"][move] - positions["nimfish"][move]
|
||||
print(f" - {move}: expected {positions['stockfish'][move]}, got {positions['nimfish'][move]} ({'-' if missed > 0 else '+'}{abs(missed)})")
|
||||
|
@ -155,4 +155,5 @@ if __name__ == "__main__":
|
|||
parser.add_argument("--bulk", action="store_true", help="Enable bulk-counting for Nimfish (faster, less debuggable)", 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("--auto-mode", action="store_true", help="Automatically attempt to detect which moves Nimfish got wrong")
|
||||
sys.exit(main(parser.parse_args()))
|
Loading…
Reference in New Issue