Move the mailbox into the position object and get rid of update(). Minor UI tweaks

This commit is contained in:
Mattia Giambirtone 2024-05-01 18:02:17 +02:00
parent da82878ebb
commit d4fe999567
6 changed files with 116 additions and 155 deletions

View File

@ -4,3 +4,4 @@
--passL:"-flto"
--passC:"-flto -march=native -mtune=native"
--mm:atomicArc
--deepCopy

View File

@ -35,8 +35,6 @@ type
Chessboard* = object
## A chessboard
# The actual board where pieces live
grid*: array[64, Piece]
# The current position
position*: Position
# List of all previously reached positions
@ -51,14 +49,9 @@ proc hash*(self: var Chessboard)
proc newChessboard*: Chessboard =
## Returns a new, empty chessboard
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
for i in Square(0)..Square(63):
result.position.mailbox[i] = nullPiece()
func getBitboard*(self: Chessboard, kind: PieceKind, color: PieceColor): Bitboard {.inline.} =
@ -102,7 +95,7 @@ proc newChessboardFromFEN*(fen: string): Chessboard =
let square = makeSquare(row, column)
piece = c.fromChar()
result.position.pieces[piece.color][piece.kind].setBit(square)
result.grid[square] = piece
result.position.mailbox[square] = piece
inc(column)
of '/':
# Next row
@ -190,7 +183,7 @@ func countPieces*(self: Chessboard, kind: PieceKind, color: PieceColor): int {.i
func getPiece*(self: Chessboard, square: Square): Piece {.inline.} =
## Gets the piece at the given square
return self.grid[square]
return self.position.mailbox[square]
func getPiece*(self: Chessboard, square: string): Piece {.inline.} =
@ -218,7 +211,7 @@ proc spawnPiece*(self: var Chessboard, square: Square, piece: Piece) =
when not defined(danger):
doAssert self.getPiece(square).kind == Empty
self.addPieceToBitboard(square, piece)
self.grid[square] = piece
self.position.mailbox[square] = piece
proc removePiece*(self: var Chessboard, square: Square) =
@ -228,7 +221,7 @@ proc removePiece*(self: var Chessboard, square: Square) =
let piece = self.getPiece(square)
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
self.removePieceFromBitboard(square)
self.grid[square] = nullPiece()
self.position.mailbox[square] = nullPiece()
proc movePiece*(self: var Chessboard, move: Move) =
@ -457,46 +450,13 @@ proc canCastle*(self: Chessboard): tuple[queen, king: bool] =
break
proc update*(self: var 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)]
let piece = self.position.mailbox[makeSquare(i, j)]
if piece.kind == Empty:
result &= "x "
continue
@ -521,7 +481,7 @@ proc pretty*(self: Chessboard): string =
result &= "\x1b[39;44;1m"
else:
result &= "\x1b[39;40;1m"
let piece = self.grid[makeSquare(i, j)]
let piece = self.position.mailbox[makeSquare(i, j)]
if piece.kind == Empty:
result &= " \x1b[0m"
else:
@ -541,7 +501,7 @@ proc toFEN*(self: Chessboard): string =
for i in 0..7:
skip = 0
for j in 0..7:
let piece = self.grid[makeSquare(i, j)]
let piece = self.position.mailbox[makeSquare(i, j)]
if piece.kind == Empty:
inc(skip)
elif skip > 0:

View File

@ -373,7 +373,8 @@ proc doMove*(self: var Chessboard, move: Move) =
enPassantSquare: enPassantTarget,
pieces: self.position.pieces,
castlingAvailability: self.position.castlingAvailability,
zobristKey: self.position.zobristKey
zobristKey: self.position.zobristKey,
mailbox: self.position.mailbox
)
if self.position.enPassantSquare != nullSquare():
self.position.zobristKey = self.position.zobristKey xor getEnPassantKey(fileFromSquare(move.targetSquare))
@ -485,7 +486,6 @@ proc unmakeMove*(self: var Chessboard) =
if self.positions.len() == 0:
return
self.position = self.positions.pop()
self.update()
## Testing stuff
@ -494,10 +494,12 @@ proc unmakeMove*(self: var Chessboard) =
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:

View File

@ -49,8 +49,18 @@ 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
# Overridden operators for our distinct type
func `==`*(a, b: Square): bool {.inline.} = a.int8 == b.int8
func `!=`*(a, b: Square): bool {.inline.} = a.int8 != b.int8
func `<`*(a: Square, b: SomeInteger): bool {.inline.} = a.int8 < b.int8
func `>`*(a: SomeInteger, b: Square): bool {.inline.} = a.int8 > b.int8
func `<=`*(a: Square, b: SomeInteger): bool {.inline.} = a.int8 <= b.int8
func `>=`*(a: SomeInteger, b: Square): bool {.inline.} = a.int8 >= b.int8
func `<`*(a, b: Square): bool {.inline.} = a.int8 < b.int8
func `>`*(a, b: Square): bool {.inline.} = a.int8 > b.int8
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)

View File

@ -43,15 +43,18 @@ type
sideToMove*: PieceColor
# Positional bitboards for all pieces
pieces*: array[PieceColor.White..PieceColor.Black, array[PieceKind.Bishop..PieceKind.Rook, 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)
# Pin rays for the current side to move
diagonalPins*: Bitboard # Rays from a bishop or queen
orthogonalPins*: Bitboard # Rays from a rook or queen
# Pieces checking the current side to move
checkers*: Bitboard
# Zobrist hash of this position
zobristKey*: ZobristKey
# Cached result of drawByRepetition()
repetitionDraw*: bool
# A mailbox for fast piece lookup by
# location
mailbox*: array[Square(0)..Square(63), Piece]
func getKingStartingSquare*(color: PieceColor): Square {.inline.} =

View File

@ -44,20 +44,8 @@ proc perft*(board: var Chessboard, ply: int, verbose = false, divide = false, bu
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"
echo &"{move.toAlgebraic()}: 1"
if verbose:
echo ""
return (uint64(len(moves)), 0, 0, 0, 0, 0, 0)
@ -109,20 +97,7 @@ proc perft*(board: var Chessboard, ply: int, verbose = false, divide = false, bu
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}"
echo &"{move.toAlgebraic()}: {next.nodes}"
if verbose:
echo ""
result.nodes += next.nodes
@ -134,63 +109,6 @@ proc perft*(board: var Chessboard, ply: int, verbose = false, divide = false, bu
result.checkmates += next.checkmates
proc handleGoCommand(board: var 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
captures = false
if args.len() > 1:
var ok = true
for arg in args[1..^1]:
case arg:
of "bulk":
bulk = true
of "verbose":
verbose = true
of "captures":
captures = 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, capturesOnly=captures).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, capturesOnly=captures)
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: var Chessboard, command: seq[string]): Move {.discardable.} =
if len(command) != 2:
echo &"Error: move: invalid number of arguments"
@ -253,6 +171,70 @@ proc handleMoveCommand(board: var Chessboard, command: seq[string]): Move {.disc
echo &"Error: move: {moveString} is illegal"
proc handleGoCommand(board: var 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
captures = false
divide = true
if args.len() > 1:
var ok = true
for arg in args[1..^1]:
case arg:
of "bulk":
bulk = true
of "verbose":
verbose = true
of "captures":
captures = true
of "nosplit":
divide = false
else:
echo &"Error: go: {command[1]}: 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=divide, bulk=true, verbose=verbose, capturesOnly=captures).nodes
let tot = cpuTime() - t
if divide:
echo ""
echo &"Nodes 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=divide, verbose=verbose, capturesOnly=captures)
let tot = cpuTime() - t
if divide:
echo ""
echo &"Nodes 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: {command[1]}: invalid depth"
else:
echo &"error: go: unknown subcommand '{command[1]}'"
proc handlePositionCommand(board: var Chessboard, command: seq[string]) =
if len(command) < 2:
echo "Error: position: invalid number of arguments"
@ -322,9 +304,9 @@ proc handlePositionCommand(board: var Chessboard, command: seq[string]) =
return
const HELP_TEXT = """Nimfish help menu:
- go: Begin a search
- go: Begin a search. Currently does not implement UCI search features (simply
switch to UCI mode for that)
Subcommands:
- perft <depth> [options]: Run the performance test at the given depth (in ply) and
print the results
@ -332,22 +314,23 @@ const HELP_TEXT = """Nimfish help menu:
- bulk: Enable bulk-counting (significantly faster, gives less statistics)
- verbose: Enable move debugging (for each and every move, not recommended on large searches)
- captures: Only generate capture moves
- nosplit: Do not print the number of legal moves after each root move
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
- kiwipete: Set the board to the 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
- moves {moveList}: Perform the given moves 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
- 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
@ -361,15 +344,14 @@ const HELP_TEXT = """Nimfish help menu:
- 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
- pins: Print the current pin masks, if any
- checks: Print the current check mask, if in check
- skip: Make a null move (i.e. pass your turn). Useful for debugging. Very much illegal
- uci: enter UCI mode
- quit: exit
- zobrist: Print the zobrist key for the current position
- quit: exit nimfish
- zobrist: Print the zobrist hash for the current position
- eval: Evaluate the current position
- rep: Show whether this position is a draw by repetition
"""
- rep: Show whether this position is a draw by repetition"""
proc commandLoop*: int =
@ -440,8 +422,10 @@ proc commandLoop*: int =
echo "error: get: invalid square"
continue
of "castle":
let castleRights = board.position.castlingAvailability[board.position.sideToMove]
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\")}"
echo &"Castling rights for {($board.position.sideToMove).toLowerAscii()}:\n - King side: {(if castleRights.king: \"yes\" else: \"no\")}\n - Queen side: {(if castleRights.queen: \"yes\" else: \"no\")}"
echo &"{($board.position.sideToMove)} can currently castle:\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":
@ -450,13 +434,14 @@ proc commandLoop*: int =
if board.position.diagonalPins != 0:
echo &"Diagonal pins:\n{board.position.diagonalPins}"
of "checks":
echo board.position.checkers
if board.position.checkers != 0:
echo board.position.checkers
of "quit":
return 0
of "zobrist":
echo board.position.zobristKey.uint64
of "rep":
echo board.position.repetitionDraw
echo "Position is drawn by repetition: ", if board.position.repetitionDraw: "yes" else: "no"
of "eval":
echo &"Eval: {board.evaluate()}"
else: