335 lines
8.3 KiB
Swift
335 lines
8.3 KiB
Swift
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) {
|
|
|
|
}
|
|
|
|
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<Square.Position>] =
|
|
[
|
|
.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]
|
|
}
|
|
}
|