diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 809b1df..091ab8b 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -13,12 +13,17 @@ # limitations under the License. import ../util/matrix +export matrix import std/strutils import std/strformat type + Location = tuple[row, col: int] + Pieces = tuple[king: Location, queen: Location, rooks: array[2, Location], + bishops: array[2, Location], knights: array[2, Location], + pawns: array[8, Location]] PieceColor* = enum None = 0, White, @@ -36,10 +41,10 @@ type kind*: PieceKind Position* = object piece*: Piece - location*: tuple[row, col: int] + location*: Location ChessBoard* = ref object ## A chess board object - grid: Matrix[Piece] + grid*: Matrix[Piece] # Currently active color turn: PieceColor # Number of half moves since @@ -55,6 +60,11 @@ type # If en passant is not possible, both the row and # column of the position will be set to -1 enPassantSquare: Position + # Locations of all pieces + pieces: tuple[white: Pieces, black: Pieces] + # Locations of all attacked squares + attacked: tuple[white: seq[Location], black: seq[Location]] + # Initialized only once, copied every time var empty: seq[Piece] = @[] @@ -62,12 +72,46 @@ for _ in countup(0, 63): empty.add(Piece(kind: Empty, color: None)) -proc algebraicToPosition(s: string): tuple[row, col: int] {.inline.} = +proc algebraicToPosition(s: string): Location {.inline.} = ## Converts a square location from algebraic ## notation to its corresponding row and column ## in the chess grid (0 indexed) - assert len(s) == 2 - result = (int(uint8(s[0]) - (uint8('a') - 1)), int(uint8(s[1]) - uint8('0'))) + 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]}')") + let file = int(uint8(s[0]) - uint8('a')) + var rank: int + case s[1]: + of '1': + rank = 7 + of '2': + rank = 6 + of '3': + rank = 5 + of '4': + rank = 4 + of '5': + rank = 3 + of '6': + rank = 2 + of '7': + rank = 1 + of '8': + rank = 0 + else: + discard + return (rank, file) + +proc getPiece*(self: ChessBoard, square: string): Piece = + ## Gets the piece on the given square + ## in algebraic notation + let loc = square.algebraicToPosition() + return self.grid[loc.row, loc.col] proc `$`*(self: ChessBoard): string = @@ -84,12 +128,33 @@ proc `$`*(self: ChessBoard): string = result &= "\n- - - - - - - -" +proc newChessboard: ChessBoard = + ## Returns a new, empty chessboard + new(result) + # Initialize all positions to a known default state. + # This is useful in newChessBoardFromFEN + result.pieces.black.king = (-1, -1) + result.pieces.white.king = (-1, -1) + result.pieces.black.queen = (-1, -1) + result.pieces.white.queen = (-1, -1) + result.pieces.black.rooks = [(-1, -1), (-1, -1)] + result.pieces.white.rooks = [(-1, -1), (-1, -1)] + result.pieces.black.bishops = [(-1, -1), (-1, -1)] + result.pieces.white.bishops = [(-1, -1), (-1, -1)] + result.pieces.black.knights = [(-1, -1), (-1, -1)] + result.pieces.white.knights = [(-1, -1), (-1, -1)] + result.pieces.black.pawns = [(-1, -1), (-1, -1), (-1, -1), (-1, -1), (-1, -1), (-1, -1), (-1, -1), (-1, -1)] + result.pieces.white.pawns = [(-1, -1), (-1, -1), (-1, -1), (-1, -1), (-1, -1), (-1, -1), (-1, -1), (-1, -1)] + # Turns our flat sequence into an 8x8 grid + result.grid = newMatrixFromSeq[Piece](empty, (8, 8)) + result.attacked = (@[], @[]) + result.enPassantSquare = Position(piece: Piece(kind: Empty, color: None), location: (-1, -1)) + + proc newChessboardFromFEN*(state: string): ChessBoard = ## Initializes a chessboard with the ## state encoded by the given FEN string - new(result) - # Turns our flat sequence into an 8x8 grid - result.grid = newMatrixFromSeq[Piece](empty, (8, 8)) + result = newChessboard() var # Current location in the grid row = 0 @@ -98,6 +163,8 @@ proc newChessboardFromFEN*(state: string): ChessBoard = section = 0 # Current index into the FEN string index = 0 + # Temporary variable to store the piece + piece: Piece # See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation while index <= state.high(): var c = state[index] @@ -110,13 +177,82 @@ proc newChessboardFromFEN*(state: string): ChessBoard = of 0: # Piece placement data case c.toLowerAscii(): + # Piece of 'r', 'n', 'b', 'q', 'k', 'p': - result.grid[row, column] = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black) + # 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: + of Pawn: + # Find first empty slot in the pieces array + for i, e in result.pieces.black.pawns: + if e == (-1, -1): + result.pieces.black.pawns[i] = (row, column) + break + of Bishop: + if result.pieces.black.bishops[0] == (-1, -1): + result.pieces.black.bishops[0] = (row, column) + else: + result.pieces.black.bishops[1] = (row, column) + of Knight: + if result.pieces.black.knights[0] == (-1, -1): + result.pieces.black.knights[0] = (row, column) + else: + result.pieces.black.knights[1] = (row, column) + of Rook: + if result.pieces.black.rooks[0] == (-1, -1): + result.pieces.black.rooks[0] = (row, column) + else: + result.pieces.black.rooks[1] = (row, column) + of Queen: + result.pieces.black.queen = (row, column) + of King: + result.pieces.black.king = (row, column) + else: + discard + of White: + case piece.kind: + of Pawn: + for i, e in result.pieces.white.pawns: + if e == (-1, -1): + result.pieces.white.pawns[i] = (row, column) + break + of Bishop: + if result.pieces.white.bishops[0] == (-1, -1): + result.pieces.white.bishops[0] = (row, column) + else: + result.pieces.white.bishops[1] = (row, column) + of Knight: + if result.pieces.white.knights[0] == (-1, -1): + result.pieces.white.knights[0] = (row, column) + else: + result.pieces.white.knights[1] = (row, column) + of Rook: + if result.pieces.white.rooks[0] == (-1, -1): + result.pieces.white.rooks[0] = (row, column) + else: + result.pieces.white.rooks[1] = (row, column) + of Queen: + result.pieces.white.queen = (row, column) + of King: + result.pieces.white.king = (row, column) + else: + discard + else: + discard + result.grid[row, column] = piece inc(column) of '/': + # Next row inc(row) column = 0 of '0'..'9': + # Skip x columns let x = int(uint8(c) - uint8('0')) - 1 if x > 7: raise newException(ValueError, "invalid skip value (> 8) in FEN string") @@ -154,7 +290,8 @@ proc newChessboardFromFEN*(state: string): ChessBoard = # En passant target square case c: of '-': - result.enPassantSquare.location = (-1, -1) + # Field is already uninitialized to the correct state + discard else: result.enPassantSquare.location = state[index..index+1].algebraicToPosition() # Just for cleanliness purposes, we fill in the other positional metadata as diff --git a/src/Chess/player.nim b/src/Chess/player.nim index b6010e5..3205d4d 100644 --- a/src/Chess/player.nim +++ b/src/Chess/player.nim @@ -1,5 +1,10 @@ import board +var b = newDefaultChessboard() +echo b +echo b.getPiece("a2") +echo b.getPiece("a7") +echo b.getPiece("a1") +echo b.getPiece("a8") -echo newChessboardFromFEN("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")[]