diff --git a/src/Chess/board.nim b/src/Chess/board.nim new file mode 100644 index 0000000..e7a658f --- /dev/null +++ b/src/Chess/board.nim @@ -0,0 +1,170 @@ +# Copyright 2023 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 ../util/matrix + + +import std/strutils + + +type + PieceColor* = enum + None = 0, + White, + Black + PieceKind* = enum + Empty = 0, # No piece + Bishop = 'b', + King = 'k' + Knight = 'n', + Pawn = 'p', + Queen = 'q', + Rook = 'r', + Piece* = object + color*: PieceColor + kind*: PieceKind + Position* = object + piece*: Piece + location*: tuple[row, col: int] + Board* = ref object + grid: Matrix[Piece] + ply: PieceColor + # Number of half moves since + # last piece capture. Used + # for the 50-move rule + halfMoveClock: int + # Full move counter. Increments + # every 2 ply + fullMoveCount: int + # Stores metadata for castling. + castling: tuple[white, black: tuple[queen, king: bool]] + # En passant target square (see https://en.wikipedia.org/wiki/En_passant) + # If en passant is not possible, both the row and + # column of the position will be set to -1 + enPassantSquare: Position + +# Initialized only once, copied every time +var empty: seq[Piece] = @[] +for _ in countup(0, 63): + empty.add(Piece(kind: Empty, color: None)) + + +proc algebraicToPosition(s: string): tuple[row, col: int] {.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'))) + + +proc newChessboardFromFEN*(state: string): Board = + ## 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)) + var + # Current location in the grid + row = 0 + column = 0 + # Current section in the FEN string + section = 0 + # Current index into the FEN string + index = 0 + # See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation + while index <= state.high(): + var c = state[index] + if c == ' ': + # Next section + inc(section) + inc(index) + continue + case section: + of 0: + # Piece placement data + case c.toLowerAscii(): + of 'r', 'n', 'b', 'q', 'k', 'p': + result.grid[row, column] = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black) + inc(column) + of '/': + inc(row) + column = 0 + of '0'..'9': + let x = int(uint8(c) - uint8('0')) - 1 + if x > 7: + raise newException(ValueError, "invalid skip value (> 8) in FEN string") + column += x + else: + raise newException(ValueError, "invalid piece identifier in FEN string") + of 1: + # Active color + case c: + of 'w': + result.ply = White + of 'b': + result.ply = Black + else: + raise newException(ValueError, "invalid next move identifier in FEN string") + of 2: + # Castling availability + case c: + of '-': + # Neither side can castle anywhere: do nothing, + # as the castling metadata is set to this state + # by default + discard + of 'K': + result.castling.white.king = true + of 'Q': + result.castling.white.queen = true + of 'k': + result.castling.black.king = true + of 'q': + result.castling.black.queen = true + else: + raise newException(ValueError, "invalid castling availability in FEN string") + of 3: + # En passant target square + case c: + of '-': + result.enPassantSquare.location = (-1, -1) + else: + result.enPassantSquare.location = state[index..index+1].algebraicToPosition() + # Just for cleanliness purposes, we fill in the other positional metadata as + # well + result.enPassantSquare.piece.color = if result.ply == Black: White else: Black + result.enPassantSquare.piece.kind = Pawn + # Square metadata is 2 bytes long + inc(index) + of 4: + # Halfmove clock + var s = "" + while not state[index].isSpaceAscii(): + s.add(state[index]) + inc(index) + # Backtrack so the space is seen by the + # next iteration of the loop + dec(index) + result.halfMoveClock = parseInt(s) + of 5: + # Fullmove number + var s = "" + while index <= state.high(): + s.add(state[index]) + inc(index) + result.fullMoveCount = parseInt(s) + else: + raise newException(ValueError, "too many fields in FEN string") + inc(index) + + \ No newline at end of file diff --git a/src/Chess/player.nim b/src/Chess/player.nim new file mode 100644 index 0000000..b6010e5 --- /dev/null +++ b/src/Chess/player.nim @@ -0,0 +1,5 @@ +import board + + + +echo newChessboardFromFEN("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")[]