179 lines
5.2 KiB
Nim
179 lines
5.2 KiB
Nim
# 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 pieces
|
|
import magics
|
|
import moves
|
|
import rays
|
|
import bitboards
|
|
import position
|
|
import zobrist
|
|
|
|
|
|
|
|
export pieces, position, bitboards, moves, magics, rays, zobrist
|
|
|
|
|
|
|
|
type
|
|
Chessboard* = object
|
|
## A chessboard
|
|
|
|
# The current position
|
|
position*: Position
|
|
# List of all previously reached positions
|
|
positions*: seq[Position]
|
|
|
|
|
|
func toFEN*(self: Chessboard): string
|
|
|
|
|
|
proc newChessboard*: Chessboard =
|
|
## Returns a new, empty chessboard
|
|
result.position = Position(enPassantSquare: nullSquare(), sideToMove: White)
|
|
for i in Square(0)..Square(63):
|
|
result.position.mailbox[i] = nullPiece()
|
|
|
|
|
|
proc newChessboardFromFEN*(fen: string): Chessboard =
|
|
## Initializes a chessboard with the
|
|
## position encoded by the given FEN string
|
|
result = newChessboard()
|
|
result.position = loadFEN(fen)
|
|
|
|
|
|
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 inCheck*(self: Chessboard): bool {.inline.} =
|
|
## Returns if the current side to move is in check
|
|
return self.position.inCheck()
|
|
|
|
|
|
proc canCastle*(self: Chessboard): tuple[queen, king: bool] {.inline.} =
|
|
## Returns if the current side to move can castle
|
|
return self.position.canCastle()
|
|
|
|
|
|
func `$`*(self: Chessboard): string = $self.position
|
|
|
|
|
|
func pretty*(self: Chessboard): string =
|
|
## Returns a colored version of the
|
|
## current position for easier visualization
|
|
return self.position.pretty()
|
|
|
|
|
|
func toFEN*(self: Chessboard): string =
|
|
## Returns a FEN string of the current
|
|
## position in the chessboard
|
|
return self.position.toFEN()
|
|
|
|
|
|
func drawnByRepetition*(self: Chessboard): bool =
|
|
## Returns whether the current position is a draw
|
|
## by repetition
|
|
# TODO: Improve this
|
|
var i = self.positions.high()
|
|
var count = 0
|
|
while i >= 0:
|
|
if self.position.zobristKey == self.positions[i].zobristKey:
|
|
inc(count)
|
|
if count == 2:
|
|
return true
|
|
if self.positions[i].halfMoveClock == 0:
|
|
# Position was reached via a pawn move or
|
|
# capture: cannot repeat beyond this point!
|
|
return false
|
|
dec(i)
|
|
|
|
|
|
proc isInsufficientMaterial*(self: Chessboard): bool =
|
|
## Returns whether the current position is drawn
|
|
## due to insufficient mating material. Note that
|
|
## this is not a strict implementation of the FIDE
|
|
## rule about material draws due to the fact that
|
|
## it would be basically impossible to implement those
|
|
## efficiently
|
|
|
|
# Break out early if there's more than 4 pieces on the
|
|
# board
|
|
let occupancy = self.position.getOccupancy()
|
|
if occupancy.countSquares() > 4:
|
|
return false
|
|
|
|
# KvK is a draw
|
|
if occupancy.countSquares() == 2:
|
|
# Only the two kings are left
|
|
return true
|
|
|
|
let
|
|
sideToMove = self.position.sideToMove
|
|
nonSideToMove = sideToMove.opposite()
|
|
|
|
# Break out early if there's any pawns left on the board
|
|
if self.position.getBitboard(Pawn, sideToMove) != 0:
|
|
return false
|
|
if self.position.getBitboard(Pawn, nonSideToMove) != 0:
|
|
return false
|
|
|
|
# If there's any queens or rooks on the board, break out early too
|
|
let
|
|
friendlyQueens = self.position.getBitboard(Queen, sideToMove)
|
|
enemyQueens = self.position.getBitboard(Queen, nonSideToMove)
|
|
friendlyRooks = self.position.getBitboard(Rook, sideToMove)
|
|
enemyRooks = self.position.getBitboard(Rook, nonSideToMove)
|
|
|
|
if (friendlyQueens or enemyQueens or friendlyRooks or enemyRooks).countSquares() != 0:
|
|
return false
|
|
|
|
# KNvK is a draw
|
|
|
|
let knightCount = (self.position.getBitboard(Knight, sideToMove) or self.position.getBitboard(Knight, nonSideToMove)).countSquares()
|
|
|
|
# More than one knight (doesn't matter which side), not a draw
|
|
if knightCount > 1:
|
|
return false
|
|
|
|
# KBvK is a draw
|
|
let bishopCount = (self.position.getBitboard(Bishop, sideToMove) or self.position.getBitboard(Bishop, nonSideToMove)).countSquares()
|
|
|
|
if bishopCount + knightCount > 1:
|
|
return false
|
|
|
|
# Maybe TODO: KBBvK and KBvKB (these should be handled by search anyway)
|
|
|
|
return true
|
|
|
|
|
|
|
|
func isDrawn*(self: Chessboard): bool =
|
|
## Returns whether the given position is
|
|
## drawn
|
|
if self.position.halfMoveClock >= 100:
|
|
# Draw by 50 move rule
|
|
return true
|
|
|
|
if self.drawnByRepetition():
|
|
return true
|
|
|
|
if self.isInsufficientMaterial():
|
|
return true
|