enum Event { case kingInCheck(_ by: Piece, on: King) case piecePinned(from: Piece, on: Piece) } protocol EventDelegate { func notify(_ event: Event) func movePiece(_ piece: Piece, to dst: Square.Position) throws func addPieceToTarget(_ piece: Piece, target: Square.Position) func getSquareInfo(on pos: Square.Position) -> Square? } public class Board: CustomStringConvertible, EventDelegate { public typealias Grid = [[Square]] private enum UnicodeBar: String, CustomStringConvertible { case VerticalBar = "│" case TopHorizontalLine = "┌───┬───┬───┬───┬───┬───┬───┬───┐" case MiddleHorizontalLine = " ├───┼───┼───┼───┼───┼───┼───┼───┤" case BottomHorizonLine = " └───┴───┴───┴───┴───┴───┴───┴───┘" var description: String { return self.rawValue } } public struct TerminalColors: CustomStringConvertible, Hashable { public enum Foreground: String { case def = "39" case black = "30" case red = "31" case green = "32" case yellow = "33" case blue = "34" case magenta = "35" case cyan = "36" case white = "37" } public enum Background: String { case def = "49" case black = "40" case red = "41" case green = "42" case yellow = "43" case blue = "44" case magenta = "45" case cyan = "46" case white = "47" } public var foregroundColor: Foreground? = .def public var backgroundColor: Background? = .def public init() {} public init(fg: Foreground?, bg: Background) { foregroundColor = fg backgroundColor = bg } public var description: String { var res = "\u{001B}[" if let fc = foregroundColor { res += fc.rawValue if backgroundColor != nil { res += ";" } } if let bc = backgroundColor { res += bc.rawValue } return res + "m" } } func notify(_ event: Event) { #warning("Not implemented") } internal func addPieceToTarget(_ piece: Piece, target: Square.Position) { guard self[target] != nil else { return } squares[target.index!].targetted.insert(piece) } public private(set) var halfMoveClock: UInt8 = 0 public var fullMoveClock: UInt8 { halfMoveClock / 2 } public internal(set) var threatenedSquares: [Color: Set] = [ .White: [], .Black: [], ] public internal(set) var history = History() public private(set) var turn: Color = .White func movePiece(_ piece: Piece, to dst: Square.Position) throws { let from = piece.position history.add( .pieceMove( from: squares[from.index!], to: squares[dst.index!])) piece.position = dst squares[dst.index!].piece = piece squares[from.index!].piece = nil halfMoveClock += 1 turn = !turn fen.set( from: board, activeColor: turn, castling: .All, enPassant: fen.enPassant, halfMoveClock: halfMoveClock, fullMoveClock: fullMoveClock) } public enum MoveFailure: Error, CustomStringConvertible { case sourceSquareUnreachable(pos: Square.Position) case destinationSquareUnreachable(pos: Square.Position) case squareHasNoPiece(pos: Square.Position) case destinationIsIllegal(pos: Square.Position) case squareANIsTooLong(an: String) case squareANIsTooShort(an: String) public var description: String { return switch self { case .sourceSquareUnreachable(let pos): "The source square given \(pos.rank) \(pos.file) is unreachable." case .destinationSquareUnreachable(let pos): "The destination square given \(pos.rank) \(pos.file) is unreachable." case .squareHasNoPiece(let pos): "The square \(pos.rank) \(pos.file) doesn't have any piece." case .destinationIsIllegal(let pos): "The destination square given \(pos.rank) \(pos.file) is illegal to be moved to." case .squareANIsTooLong(let an): "The algebraic notation \(an) is too long. (Needs to be 2 characters)" case .squareANIsTooShort(let an): "The algebraic notation \(an) is too short. (Needs to be 2 characters)" } } } public func move(src: String, dst: String) throws { if let from = try Square.Position(with: src), let to = try Square.Position(with: dst) { try move(src: from, dst: to) } } public func move(src: Square.Position, dst: Square.Position) throws { guard let srcSquare = self[src] else { throw MoveFailure.sourceSquareUnreachable(pos: src) } guard self[dst] != nil else { throw MoveFailure.destinationSquareUnreachable(pos: src) } guard let piece = srcSquare.piece else { throw MoveFailure.squareHasNoPiece(pos: src) } try piece.move(to: dst) threatenedSquares = [ .White: [], .Black: [], ] for square in squares where square.piece != nil { let p = square.piece! p.getLegalPosition() threatenedSquares[p.color]! += Set(p.legalPositions) } } public func getSquareInfo(on pos: Square.Position) -> Square? { self[pos] } private var squares = [Square]() internal var board: Grid { var board = Grid() var rank = -1 for i in 0...63 { if i % 8 == 0 { board.append([]) rank += 1 } board[rank].append(squares[i]) } return board } public internal(set) var fen: Fen public func setBoard() throws { var file: Int8 = 1 var rank: Int8 = 8 var index: Int8 = 0 var b: [Square] = squares for c in fen.placement { let r = 8 - Int(rank) if c == "/" { if file != 9 { throw Fen.FenError.NotAppropriateLength( n: file, column: index) } rank -= 1 file = 0 } else if c.isWholeNumber, let n = Int8(String(c)) { if n < 1 { throw Fen.FenError.NumberTooSmall(n: n, column: index) } else if n > 8 { throw Fen.FenError.NumberTooBig(n: n, column: index) } let f = file for i in f..<(f + n) { file += 1 b[8 * r + Int(i)].piece = nil } file -= 1 } else if c.isASCII { switch Kind[c] { case .some(let (k, c)): let piece: Piece = switch k { case .Pawn: Pawn(with: c, on: .init(rank: rank, file: file)) case .Knight: Knight(with: c, on: .init(rank: rank, file: file)) case .Bishop: Bishop(with: c, on: .init(rank: rank, file: file)) case .Rook: Rook(with: c, on: .init(rank: rank, file: file)) case .Queen: Queen(with: c, on: .init(rank: rank, file: file)) case .King: King(with: c, on: .init(rank: rank, file: file)) } piece.delegate = self b[8 * r + Int(file) - 1].piece = piece case .none: throw Fen.FenError.InvalidCharacter( c: String(c), column: index) } } file += 1 index += 1 } squares = b } public required init( fen: Fen = .init( fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") ) { var rank: Int8 = 8 self.fen = fen for i in 0...63 { let index = Int8(i) let square = Square( position: .init(rank: rank, file: (index % 8) + 1), color: index % 2 != rank % 2 ? .Black : .White) squares.append(square) if (index + 1) % 8 == 0 { rank -= 1 } } #warning("Handle better.") do { try setBoard() for square in squares where square.piece != nil { let p = square.piece! p.getLegalPosition() threatenedSquares[p.color]! += Set(p.legalPositions) } } catch Fen.FenError.NotAppropriateLength(let n, let column) { fatalError("Not appropriate length: \(n) on \(column)") } catch { } } public func text(with colors: [TerminalColors: [Square.Position]]? = nil) -> String { var boardString = " A B C D E F G H \n \(UnicodeBar.TopHorizontalLine)\n" var _rank: UInt8 = 8 let def: TerminalColors = .init() var h1 = def board.forEach { rank in boardString += String(_rank) + " \(UnicodeBar.VerticalBar)" rank.forEach { square in let p = square.piece?.unicodeRepresentation ?? " " h1 = def if let c = colors { for (color, s) in c where (s.contains { $0 == square.position }) { h1 = color } } boardString += "\(h1) \(p) \(def)\(UnicodeBar.VerticalBar)" } boardString += "\n" _rank -= 1 if _rank > 0 { boardString += "\(UnicodeBar.MiddleHorizontalLine)\n" } } boardString += "\(UnicodeBar.BottomHorizonLine)" return boardString } public var description: String { text() } internal subscript(pos: Square.Position) -> Square? { guard let i = pos.index else { return nil } if i > squares.count { return nil } return squares[i] } }