diff options
-rw-r--r-- | .gitignore | 8 | ||||
-rw-r--r-- | Package.swift | 23 | ||||
-rw-r--r-- | README.md | 27 | ||||
m--------- | Sources | 0 | ||||
-rw-r--r-- | Tests/SwiftChessNeoTests/PGNParsingTests.swift | 294 | ||||
-rw-r--r-- | Tests/SwiftChessNeoTests/SwiftChessNeoTests.swift | 539 |
6 files changed, 891 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..d1695e8 --- /dev/null +++ b/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "swift-chess-neo", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "SwiftChessNeo", + targets: ["SwiftChessNeo"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "SwiftChessNeo"), + .testTarget( + name: "SwiftChessNeoTests", + dependencies: ["SwiftChessNeo"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..29e634c --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# SwiftChessNeo + +**WIP: I am actively developing swift-chess-neo while writing iWatchChess for iOS/macOS** + +Fork of [https://github.com/nvzqz/Sage](Sage by @nvzqz) along with [https://github.com/SuperGeroy](@SuperGeroy)'s patches. This fork adds SwiftUI views, and other QoL improvements. Due to some technical difficulties, I ended up copying the files in the `Sources` folder and adding them to my project. + +## To-Do + +- [ ] SwiftUI Views (In-Progress) +- [ ] UCI Chess Engine Support +- [ ] SVG Resources +- Move Handling + - [ ] Enhance PGN Parsing + - [ ] Comprehensive PGN Support + - [ ] Support for different lines +- [ ] GameplayKit Support + + +### Possible Misc Enhancements + +- Integrated Lichess Client (?) +- Player Database (?) + +## License + +Sage and its modifications are published under [version 2.0 of the Apache License](https://www.apache.org/licenses/LICENSE-2.0). + diff --git a/Sources b/Sources new file mode 160000 +Subproject f2992614649a28faddf87301952dc194c1fae2c diff --git a/Tests/SwiftChessNeoTests/PGNParsingTests.swift b/Tests/SwiftChessNeoTests/PGNParsingTests.swift new file mode 100644 index 0000000..eb2c121 --- /dev/null +++ b/Tests/SwiftChessNeoTests/PGNParsingTests.swift @@ -0,0 +1,294 @@ +// +// PGNParsingTests.swift +// Sage +// +// Created by Kajetan Dąbrowski on 07/10/2016. +// Copyright © 2016 Nikolai Vazquez. All rights reserved. +// + +import Foundation +import XCTest +@testable import SwiftChessNeo + +class PGNParsingTests: XCTestCase { + + let moves: [PGNMove] = ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nge7", "O-O", "f6", "Qe2", "d5", + "b3", "Qd6", "Na3", "Be6", "Rb1", "O-O-O", "Bxd5", "Qxd5", "exd5", "Nf5", + "d6", "Nfd4", "d7+", "Kb8", "Qa6", "Re8", "d8=Q+", "Nxd8", "Qxa7+", "Kxa7", + "Bb2", "N8c6", "Rfc1", "Rd8", "Ra1", "Rd5", "Rf1", "Bxa3", "Nxd4", "e4", + "f4", "exf3", "Rxf3", "Rhd8", "d3", "R5d6", "Bxa3", "Nxd4", "Bxd6", "Rf8", + "Bxf8", "Nc6", "Rf5", "Bf7", "Bc5+", "Ka8", "Rd5", "Nd8", "Rxd8#"] + + let fens: [String] = ["rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", + "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2", + "rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2", + "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", + "r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3", + "r1bqkb1r/ppppnppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4", + "r1bqkb1r/ppppnppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 b kq - 5 4", + "r1bqkb1r/ppppn1pp/2n2p2/4p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 w kq - 0 5", + "r1bqkb1r/ppppn1pp/2n2p2/4p3/2B1P3/5N2/PPPPQPPP/RNB2RK1 b kq - 1 5", + "r1bqkb1r/ppp1n1pp/2n2p2/3pp3/2B1P3/5N2/PPPPQPPP/RNB2RK1 w kq d6 0 6", + "r1bqkb1r/ppp1n1pp/2n2p2/3pp3/2B1P3/1P3N2/P1PPQPPP/RNB2RK1 b kq - 0 6", + "r1b1kb1r/ppp1n1pp/2nq1p2/3pp3/2B1P3/1P3N2/P1PPQPPP/RNB2RK1 w kq - 1 7", + "r1b1kb1r/ppp1n1pp/2nq1p2/3pp3/2B1P3/NP3N2/P1PPQPPP/R1B2RK1 b kq - 2 7", + "r3kb1r/ppp1n1pp/2nqbp2/3pp3/2B1P3/NP3N2/P1PPQPPP/R1B2RK1 w kq - 3 8", + "r3kb1r/ppp1n1pp/2nqbp2/3pp3/2B1P3/NP3N2/P1PPQPPP/1RB2RK1 b kq - 4 8", + "2kr1b1r/ppp1n1pp/2nqbp2/3pp3/2B1P3/NP3N2/P1PPQPPP/1RB2RK1 w - - 5 9", + "2kr1b1r/ppp1n1pp/2nqbp2/3Bp3/4P3/NP3N2/P1PPQPPP/1RB2RK1 b - - 0 9", + "2kr1b1r/ppp1n1pp/2n1bp2/3qp3/4P3/NP3N2/P1PPQPPP/1RB2RK1 w - - 0 10", + "2kr1b1r/ppp1n1pp/2n1bp2/3Pp3/8/NP3N2/P1PPQPPP/1RB2RK1 b - - 0 10", + "2kr1b1r/ppp3pp/2n1bp2/3Ppn2/8/NP3N2/P1PPQPPP/1RB2RK1 w - - 1 11", + "2kr1b1r/ppp3pp/2nPbp2/4pn2/8/NP3N2/P1PPQPPP/1RB2RK1 b - - 0 11", + "2kr1b1r/ppp3pp/2nPbp2/4p3/3n4/NP3N2/P1PPQPPP/1RB2RK1 w - - 1 12", + "2kr1b1r/pppP2pp/2n1bp2/4p3/3n4/NP3N2/P1PPQPPP/1RB2RK1 b - - 0 12", + "1k1r1b1r/pppP2pp/2n1bp2/4p3/3n4/NP3N2/P1PPQPPP/1RB2RK1 w - - 1 13", + "1k1r1b1r/pppP2pp/Q1n1bp2/4p3/3n4/NP3N2/P1PP1PPP/1RB2RK1 b - - 2 13", + "1k2rb1r/pppP2pp/Q1n1bp2/4p3/3n4/NP3N2/P1PP1PPP/1RB2RK1 w - - 3 14", + "1k1Qrb1r/ppp3pp/Q1n1bp2/4p3/3n4/NP3N2/P1PP1PPP/1RB2RK1 b - - 0 14", + "1k1nrb1r/ppp3pp/Q3bp2/4p3/3n4/NP3N2/P1PP1PPP/1RB2RK1 w - - 0 15", + "1k1nrb1r/Qpp3pp/4bp2/4p3/3n4/NP3N2/P1PP1PPP/1RB2RK1 b - - 0 15", + "3nrb1r/kpp3pp/4bp2/4p3/3n4/NP3N2/P1PP1PPP/1RB2RK1 w - - 0 16", + "3nrb1r/kpp3pp/4bp2/4p3/3n4/NP3N2/PBPP1PPP/1R3RK1 b - - 1 16", + "4rb1r/kpp3pp/2n1bp2/4p3/3n4/NP3N2/PBPP1PPP/1R3RK1 w - - 2 17", + "4rb1r/kpp3pp/2n1bp2/4p3/3n4/NP3N2/PBPP1PPP/1RR3K1 b - - 3 17", + "3r1b1r/kpp3pp/2n1bp2/4p3/3n4/NP3N2/PBPP1PPP/1RR3K1 w - - 4 18", + "3r1b1r/kpp3pp/2n1bp2/4p3/3n4/NP3N2/PBPP1PPP/R1R3K1 b - - 5 18", + "5b1r/kpp3pp/2n1bp2/3rp3/3n4/NP3N2/PBPP1PPP/R1R3K1 w - - 6 19", + "5b1r/kpp3pp/2n1bp2/3rp3/3n4/NP3N2/PBPP1PPP/R4RK1 b - - 7 19", + "7r/kpp3pp/2n1bp2/3rp3/3n4/bP3N2/PBPP1PPP/R4RK1 w - - 0 20", + "7r/kpp3pp/2n1bp2/3rp3/3N4/bP6/PBPP1PPP/R4RK1 b - - 0 20", + "7r/kpp3pp/2n1bp2/3r4/3Np3/bP6/PBPP1PPP/R4RK1 w - - 0 21", + "7r/kpp3pp/2n1bp2/3r4/3NpP2/bP6/PBPP2PP/R4RK1 b - f3 0 21", + "7r/kpp3pp/2n1bp2/3r4/3N4/bP3p2/PBPP2PP/R4RK1 w - - 0 22", + "7r/kpp3pp/2n1bp2/3r4/3N4/bP3R2/PBPP2PP/R5K1 b - - 0 22", + "3r4/kpp3pp/2n1bp2/3r4/3N4/bP3R2/PBPP2PP/R5K1 w - - 1 23", + "3r4/kpp3pp/2n1bp2/3r4/3N4/bP1P1R2/PBP3PP/R5K1 b - - 0 23", + "3r4/kpp3pp/2nrbp2/8/3N4/bP1P1R2/PBP3PP/R5K1 w - - 1 24", + "3r4/kpp3pp/2nrbp2/8/3N4/BP1P1R2/P1P3PP/R5K1 b - - 0 24", + "3r4/kpp3pp/3rbp2/8/3n4/BP1P1R2/P1P3PP/R5K1 w - - 0 25", + "3r4/kpp3pp/3Bbp2/8/3n4/1P1P1R2/P1P3PP/R5K1 b - - 0 25", + "5r2/kpp3pp/3Bbp2/8/3n4/1P1P1R2/P1P3PP/R5K1 w - - 1 26", + "5B2/kpp3pp/4bp2/8/3n4/1P1P1R2/P1P3PP/R5K1 b - - 0 26", + "5B2/kpp3pp/2n1bp2/8/8/1P1P1R2/P1P3PP/R5K1 w - - 1 27", + "5B2/kpp3pp/2n1bp2/5R2/8/1P1P4/P1P3PP/R5K1 b - - 2 27", + "5B2/kpp2bpp/2n2p2/5R2/8/1P1P4/P1P3PP/R5K1 w - - 3 28", + "8/kpp2bpp/2n2p2/2B2R2/8/1P1P4/P1P3PP/R5K1 b - - 4 28", + "k7/1pp2bpp/2n2p2/2B2R2/8/1P1P4/P1P3PP/R5K1 w - - 5 29", + "k7/1pp2bpp/2n2p2/2BR4/8/1P1P4/P1P3PP/R5K1 b - - 6 29", + "k2n4/1pp2bpp/5p2/2BR4/8/1P1P4/P1P3PP/R5K1 w - - 7 30", + "k2R4/1pp2bpp/5p2/2B5/8/1P1P4/P1P3PP/R5K1 b - - 0 30"] + + func testGameParsingPGNStyleMoves() throws { + XCTAssertEqual(fens.count, moves.count + 1) + } + + func testAllMovesInInitialPosition() { + let initialPosition = Game.Position() + let possibleMoves: [PGNMove] = ["a3", "a4", "b3", "b4", "Nf3", "e4", "e3", "d4", "Nc3", "Na3", "h3"] + #if swift(>=3) + let resultingMoves: [Move] = [Move(start: .a2, end: .a3), + Move(start: .a2, end: .a4), + Move(start: .b2, end: .b3), + Move(start: .b2, end: .b4), + Move(start: .g1, end: .f3), + Move(start: .e2, end: .e4), + Move(start: .e2, end: .e3), + Move(start: .d2, end: .d4), + Move(start: .b1, end: .c3), + Move(start: .b1, end: .a3), + Move(start: .h2, end: .h3)] + #else + let resultingMoves: [Move] = [Move(start: .A2, end: .A3), + Move(start: .A2, end: .A4), + Move(start: .B2, end: .B3), + Move(start: .B2, end: .B4), + Move(start: .G1, end: .F3), + Move(start: .E2, end: .E4), + Move(start: .E2, end: .E3), + Move(start: .D2, end: .D4), + Move(start: .B1, end: .C3), + Move(start: .B1, end: .A3), + Move(start: .H2, end: .H3)] + + #endif + for i in 0..<possibleMoves.count { + try XCTAssertEqual(PGNParser.parse(move: possibleMoves[i], in: initialPosition), resultingMoves[i]) + } + } + + #if swift(>=3) + + func testRookMovesParsing() { + let position = Game.Position(fen: "1kq5/7R/8/8/R2PR2R/8/8/4K2R w - - 0 1")! + XCTAssertEqual(try? PGNParser.parse(move: "Ra4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rb4+", in: position), Move(start: .a4, end: .b4)) + XCTAssertEqual(try? PGNParser.parse(move: "Rc4", in: position), Move(start: .a4, end: .c4)) + XCTAssertEqual(try? PGNParser.parse(move: "Rd4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Re4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rf4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rg4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rh4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Reg4", in: position), Move(start: .e4, end: .g4)) + XCTAssertEqual(try? PGNParser.parse(move: "Rhg4", in: position), Move(start: .h4, end: .g4)) + + XCTAssertEqual(try? PGNParser.parse(move: "Rh8", in: position), Move(start: .h7, end: .h8)) + XCTAssertEqual(try? PGNParser.parse(move: "Rh7", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rh6", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rh6", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rhh6", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "R7h6", in: position), Move(start: .h7, end: .h6)) + } + + #else + + func testRookMovesParsing() { + let position = Game.Position(fen: "1kq5/7R/8/8/R2PR2R/8/8/4K2R w - - 0 1")! + XCTAssertEqual(try? PGNParser.parse(move: "Ra4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rb4+", in: position), Move(start: .A4, end: .B4)) + XCTAssertEqual(try? PGNParser.parse(move: "Rc4", in: position), Move(start: .A4, end: .C4)) + XCTAssertEqual(try? PGNParser.parse(move: "Rd4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Re4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rf4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rg4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rh4", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Reg4", in: position), Move(start: .E4, end: .G4)) + XCTAssertEqual(try? PGNParser.parse(move: "Rhg4", in: position), Move(start: .H4, end: .G4)) + + XCTAssertEqual(try? PGNParser.parse(move: "Rh8", in: position), Move(start: .H7, end: .H8)) + XCTAssertEqual(try? PGNParser.parse(move: "Rh7", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rh6", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rh6", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "Rhh6", in: position), nil) + XCTAssertEqual(try? PGNParser.parse(move: "R7h6", in: position), Move(start: .H7, end: .H6)) + } + + #endif + + func testGamePlaysCorrectly() { + for i in 0..<moves.count { + do { + let startFen = fens[i] + let expectedFen = fens[i+1] + let startPosition = Game.Position(fen: startFen)! + let moveString = moves[i] + let move = try PGNParser.parse(move: moveString, in: startPosition) + let game = try Game(position: startPosition) + try game.execute(move: move) + #if swift(>=3) + var fenElements = game.position.fen().components(separatedBy: " ") + _ = fenElements.removeLast() + let finalFen = fenElements.joined(separator: " ") + var expectedFenElements = expectedFen.components(separatedBy: " ") + _ = expectedFenElements.removeLast() + let finalExpectedFen = expectedFenElements.joined(separator: " ") + #else + var fenElements = game.position.fen().componentsSeparatedByString(" ") + _ = fenElements.removeLast() + let finalFen = fenElements.joinWithSeparator(" ") + var expectedFenElements = expectedFen.componentsSeparatedByString(" ") + _ = expectedFenElements.removeLast() + let finalExpectedFen = expectedFenElements.joinWithSeparator(" ") + #endif + + XCTAssertEqual(finalFen, finalExpectedFen) + } catch { + XCTFail("\(error)") + } + } + } + + func testPGNValidMoves() { + let validMoves: [String] = ["e4", "e5", "Nf3", "Nc6", "Bc4", "Nge7", "O-O", "f6", "Qe2", "d5", + "b3", "Qd6", "Na3", "Be6", "Rb1", "O-O-O", "Bxd5", "Qxd5", "exd5", "Nf5", + "d6", "Nfd4", "d7+", "Kb8", "Qa6", "Re8", "d8=Q+", "Nxd8", "Qxa7+", "Kxa7", + "Bb2", "N8c6", "Rfc1", "Rd8", "Ra1", "Rd5", "Rf1", "Bxa3", "Nxd4", "e4", + "f4", "exf3", "Rxf3", "Rhd8", "d3", "R5d6", "Bxa3", "Nxd4", "Bxd6", "Rf8", + "Bxf8", "Nc6", "Rf5", "Bf7", "Bc5+", "Ka8", "Rd5", "Nd8", "Rxd8#"] + + let invalidMoves: [String] = ["r3gr34", "xihwr", "Ld4", "j3", "Rxf9", "😮"] + for move in validMoves { + XCTAssertNotNil(PGNMove(rawValue: move)) + XCTAssertEqual(PGNMove(rawValue: move)?.isPossible, true) + } + + for move in invalidMoves { + XCTAssertNil(PGNMove(rawValue: move)) + } + + let capture = PGNMove(rawValue: "Nxe6") + XCTAssertNotNil(capture) + XCTAssertTrue(capture!.isPossible) + XCTAssertTrue(capture!.isCapture) + XCTAssertFalse(capture!.isPromotion) + XCTAssertNil(capture!.promotionPiece) + XCTAssertFalse(capture!.isCheck) + XCTAssertFalse(capture!.isCheckmate) + XCTAssertEqual(capture!.piece, Piece.Kind._knight) + XCTAssertFalse(capture!.isCastle) + XCTAssertEqual(capture!.rank, Rank(6)) + XCTAssertEqual(capture!.file, File._e) + XCTAssertEqual(capture!.sourceRank, nil) + XCTAssertEqual(capture!.sourceFile, nil) + + let promotion: PGNMove = "d8=B#" + XCTAssertTrue(promotion.isPossible) + XCTAssertFalse(promotion.isCapture) + XCTAssertTrue(promotion.isPromotion) + XCTAssertEqual(promotion.promotionPiece, Piece.Kind._bishop) + XCTAssertTrue(promotion.isCheck) + XCTAssertTrue(promotion.isCheckmate) + XCTAssertEqual(promotion.piece, Piece.Kind._pawn) + XCTAssertFalse(promotion.isCastle) + XCTAssertEqual(promotion.rank, Rank(8)) + XCTAssertEqual(promotion.file, File._d) + XCTAssertEqual(promotion.sourceRank, nil) + XCTAssertEqual(promotion.sourceFile, nil) + + let pawnCapture: PGNMove = "axb4" + XCTAssertTrue(pawnCapture.isPossible) + XCTAssertTrue(pawnCapture.isCapture) + XCTAssertFalse(pawnCapture.isPromotion) + XCTAssertNil(pawnCapture.promotionPiece) + XCTAssertFalse(pawnCapture.isCheck) + XCTAssertFalse(pawnCapture.isCheckmate) + XCTAssertEqual(pawnCapture.piece, Piece.Kind._pawn) + XCTAssertFalse(pawnCapture.isCastle) + XCTAssertEqual(pawnCapture.rank, Rank(4)) + XCTAssertEqual(pawnCapture.file, File._b) + XCTAssertEqual(pawnCapture.sourceRank, nil) + XCTAssertEqual(pawnCapture.sourceFile, File._a) + + XCTAssertTrue(PGNMove(rawValue: "O-O-O")!.isCastleQueenside) + XCTAssertFalse(PGNMove(rawValue: "O-O-O")!.isCastleKingside) + XCTAssertTrue(PGNMove(rawValue: "O-O-O")!.isCastle) + XCTAssertFalse(PGNMove(rawValue: "O-O-O")!.isCheck) + + XCTAssertFalse(PGNMove(rawValue: "O-O+")!.isCastleQueenside) + XCTAssertTrue(PGNMove(rawValue: "O-O+")!.isCastleKingside) + XCTAssertTrue(PGNMove(rawValue: "O-O+")!.isCastle) + XCTAssertTrue(PGNMove(rawValue: "O-O+")!.isCheck) + } + + #if swift(>=3) + + func testParserShouldNotCrashOnInvalidMoves() { + let game = Game() + XCTAssertThrowsError(try game.execute(move: "aiuw")) + XCTAssertThrowsError(try game.execute(move: "")) + XCTAssertThrowsError(try game.execute(move: "a#")) + XCTAssertThrowsError(try game.execute(move: "a")) + XCTAssertThrowsError(try game.execute(move: "w")) + XCTAssertThrowsError(try game.execute(move: "!3")) + XCTAssertThrowsError(try game.execute(move: "ad")) + XCTAssertThrowsError(try game.execute(move: "aB")) + XCTAssertThrowsError(try game.execute(move: "B3")) + XCTAssertThrowsError(try game.execute(move: "x")) + XCTAssertThrowsError(try game.execute(move: "1")) + XCTAssertThrowsError(try game.execute(move: "🍣")) + XCTAssertThrowsError(try game.execute(move: "VASF234df89ayrsdfiuafiuawf")) + } + + #endif +} diff --git a/Tests/SwiftChessNeoTests/SwiftChessNeoTests.swift b/Tests/SwiftChessNeoTests/SwiftChessNeoTests.swift new file mode 100644 index 0000000..310a53c --- /dev/null +++ b/Tests/SwiftChessNeoTests/SwiftChessNeoTests.swift @@ -0,0 +1,539 @@ +import XCTest +@testable import SwiftChessNeo + +final class Sage2Tests: XCTestCase { + + func testBoardInitializer() { + XCTAssertEqual(Board(variant: .standard), Board()) + XCTAssertNotEqual(Board(variant: nil), Board()) + } + + func testBoardEquality() { + XCTAssertEqual(Board(), Board()) + XCTAssertEqual(Board(variant: nil), Board(variant: nil)) + XCTAssertNotEqual(Board(), Board(variant: nil)) + var board = Board(variant: .standard) + board.removePiece(at: .a1) + XCTAssertNotEqual(Board(), board) + } + + func testBoardPopulate() { + let board = Board() + XCTAssertEqual(board.pieces.count, 32) + XCTAssertEqual(board.whitePieces.count, 16) + XCTAssertEqual(board.blackPieces.count, 16) + for file in File.all { for rank in Rank.all { + if let piece = board[(file, rank)] { + let color = piece.color + XCTAssertTrue((color.isWhite ? [1, 2] : [7, 8]).contains(rank)) + if piece.kind.isPawn { + XCTAssertTrue([2, 7].contains(rank)) + } else { + XCTAssertTrue([1, 8].contains(rank)) + } + switch piece.kind { + case .pawn: + break + case .knight: + XCTAssertTrue([.b, .g].contains(file)) + case .bishop: + XCTAssertTrue([.c, .f].contains(file)) + case .rook: + XCTAssertTrue([.a, .h].contains(file)) + case .queen: + XCTAssertEqual(file, File.d) + case .king: + XCTAssertEqual(file, File.e) + } + } else { + XCTAssertTrue([3, 4, 5, 6].contains(rank)) + } + } } + XCTAssertTrue(Board(variant: nil).pieces.isEmpty) + } + + func testBoardFromCharacters() { + let board = Board(pieces: [["r", "n", "b", "q", "k", "b", "n", "r"], + ["p", "p", "p", "p", "p", "p", "p", "p"], + [" ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " "], + [" ", " ", " ", " ", " ", " ", " ", " "], + ["P", "P", "P", "P", "P", "P", "P", "P"], + ["R", "N", "B", "Q", "K", "B", "N", "R"]]) + XCTAssertEqual(board, Board()) + } + + func testBoardSequence() { + let board = Board() + let spaces = Array(board) + let pieces = spaces.flatMap({ $0.piece }) + let whitePieces = pieces.filter({ $0.color.isWhite }) + let blackPieces = pieces.filter({ $0.color.isBlack }) + XCTAssertEqual(spaces.count, 64) + XCTAssertEqual(pieces.count, 32) + XCTAssertEqual(whitePieces.count, 16) + XCTAssertEqual(blackPieces.count, 16) + } + + func testBoardSubscript() { + var board = Board() + #if swift(>=3) + XCTAssertEqual(Piece(pawn: .white), board[.a2]) + XCTAssertEqual(Piece(king: .black), board[.e8]) + let piece = Piece(pawn: .black) + #else + XCTAssertEqual(Piece(pawn: .White), board[.A2]) + XCTAssertEqual(Piece(king: .Black), board[.E8]) + let piece = Piece(pawn: .Black) + #endif + let location = ("A", 3) as Location + XCTAssertNil(board[location]) + board[location] = piece + XCTAssertEqual(board[location], piece) + board[location] = nil + XCTAssertNil(board[location]) + } + + func testBoardSwap() { + let start = Board() + var board = start + let location1 = ("D", 1) as Location + let location2 = ("F", 2) as Location + board.swap(location1, location2) + XCTAssertEqual(start[location1], board[location2]) + XCTAssertEqual(start[location2], board[location1]) + } + + func testAllFiles() { + XCTAssertEqual(File.all, ["a", "b", "c", "d", "e", "f", "g", "h"]) + } + + func testAllRanks() { + XCTAssertEqual(Rank.all, [1, 2, 3, 4, 5, 6, 7, 8]) + } + + func testFileOpposite() { + let all = File.all + #if swift(>=3) + let reversed = all.reversed() + #else + let reversed = all.reverse() + #endif + for (a, b) in zip(all, reversed) { + XCTAssertEqual(a.opposite(), b) + } + } + + func testRankOpposite() { + let all = Rank.all + #if swift(>=3) + let reversed = all.reversed() + #else + let reversed = all.reverse() + #endif + for (a, b) in zip(all, reversed) { + XCTAssertEqual(a.opposite(), b) + } + } + + func testFileTo() { + #if swift(>=3) + XCTAssertEqual(File.a.to(.h), File.all) + XCTAssertEqual(File.a.to(.a), [File.a]) + #else + XCTAssertEqual(File.A.to(.H), File.all) + XCTAssertEqual(File.A.to(.A), [File.A]) + #endif + } + + func testRankTo() { + #if swift(>=3) + XCTAssertEqual(Rank.one.to(.eight), Rank.all) + XCTAssertEqual(Rank.one.to(.one), [Rank.one]) + #else + XCTAssertEqual(Rank.One.to(.Eight), Rank.all) + XCTAssertEqual(Rank.One.to(.One), [Rank.One]) + #endif + } + + func testFileBetween() { + #if swift(>=3) + XCTAssertEqual(File.c.between(.f), [.d, .e]) + XCTAssertEqual(File.c.between(.d), []) + XCTAssertEqual(File.c.between(.c), []) + #else + XCTAssertEqual(File.C.between(.F), [.D, .E]) + XCTAssertEqual(File.C.between(.D), []) + XCTAssertEqual(File.C.between(.C), []) + #endif + } + + func testRankBetween() { + #if swift(>=3) + XCTAssertEqual(Rank.two.between(.five), [.three, .four]) + XCTAssertEqual(Rank.two.between(.three), []) + XCTAssertEqual(Rank.two.between(.two), []) + #else + XCTAssertEqual(Rank.Two.between(.Five), [.Three, .Four]) + XCTAssertEqual(Rank.Two.between(.Three), []) + XCTAssertEqual(Rank.Two.between(.Two), []) + #endif + } + + func testFileFromCharacter() { + for u in 65...72 { + #if swift(>=3) + let scalar = UnicodeScalar(u)! + #else + let scalar = UnicodeScalar(u) + #endif + XCTAssertNotNil(File(Character(scalar))) + } + for u in 97...104 { + #if swift(>=3) + let scalar = UnicodeScalar(u)! + #else + let scalar = UnicodeScalar(u) + #endif + XCTAssertNotNil(File(Character(scalar))) + } + } + + func testRankFromNumber() { + for n in 1...8 { + XCTAssertNotNil(Rank(n)) + } + } + + func testMoveEquality() { + #if swift(>=3) + let move = Move(start: .a1, end: .c3) + XCTAssertEqual(move, move) + XCTAssertEqual(move, Move(start: .a1, end: .c3)) + XCTAssertNotEqual(move, Move(start: .a1, end: .b1)) + #else + let move = Move(start: .A1, end: .C3) + XCTAssertEqual(move, move) + XCTAssertEqual(move, Move(start: .A1, end: .C3)) + XCTAssertNotEqual(move, Move(start: .A1, end: .B1)) + #endif + } + + func testMoveRotation() { + #if swift(>=3) + let move = Move(start: .a1, end: .c6) + let rotated = move.rotated() + XCTAssertEqual(rotated.start, Square.h8) + XCTAssertEqual(rotated.end, Square.f3) + #else + let move = Move(start: .A1, end: .C6) + let rotated = move.rotated() + XCTAssertEqual(rotated.start, Square.H8) + XCTAssertEqual(rotated.end, Square.F3) + #endif + } + + func testMoveOperator() { + for file in File.all { for rank in Rank.all { + let start = Square(file: file, rank: rank) + let end = Square(file: file.opposite(), rank: rank.opposite()) + XCTAssertEqual(Move(start: start, end: end), start >>> end) + } } + } + + func testGameRandomMoves() throws { + let game = Game() + while let move = game.availableMoves().random() { + let enemyColor = game.playerTurn.inverse() + let enemyKingSpace = game.board.squareForKing(for: enemyColor) + guard move.end != enemyKingSpace else { + let error = "Attempted attack to king for \(enemyColor): \(move.formatted())" + + "\nPosition: " + game.position.fen().debugDescription + + "\nMoves: \(game.playedMoves.formatted())" + XCTFail(error) + return + } + try game.execute(move: move) + } + guard let outcome = game.outcome else { + XCTFail("Expected outcome for complete game") + return + } + if let color = outcome.winColor { + guard game.kingIsChecked && game.board.kingIsChecked(for: color.inverse()) else { + XCTFail("\(color.inverse()) should be in check if \(color) wins") + return + } + } + } + + func testGameFromMoves() throws { + let game = Game() + while let move = game.availableMoves().random() { + try game.execute(uncheckedMove: move) + } + do { + let moves = game.playedMoves + let other = try Game(moves: moves) + XCTAssertEqual(other.board, game.board) + XCTAssertEqual(other.playedMoves, moves) + } catch { + #if swift(>=3) + let str = String(describing: error) + #else + let str = String(error) + #endif + XCTFail(str) + } + } + + func testGameDoubleStep() throws { + let game = Game() + for file in File.all { + let move = Move(start: Square(location: (file, 2)), end: Square(location: (file, 5))) + XCTAssertThrowsError(try game.execute(move: move)) { error in + #if swift(>=3) + guard case Game.ExecutionError.illegalMove = error else { + XCTFail("Expected MoveExecutionError.IllegalMove, got \(error)") + return + } + #else + guard case Game.ExecutionError.IllegalMove = error else { + XCTFail("Expected MoveExecutionError.IllegalMove, got \(error)") + return + } + #endif + } + } + for file in File.all { + try game.execute(move: Move(start: Square(location: (file, 2)), end: Square(location: (file, 4)))) + try game.execute(move: Move(start: Square(location: (file, 7)), end: Square(location: (file, 5)))) + } + } + + func testGameEnPassant() { + let game = Game() + do { + #if swift(>=3) + try game.execute(move: Move(start: .c2, end: .c4)) + try game.execute(move: Move(start: .c7, end: .c6)) + try game.execute(move: Move(start: .c4, end: .c5)) + try game.execute(move: Move(start: .d7, end: .d5)) + try game.execute(move: Move(start: .c5, end: .d6)) + #else + try game.execute(move: Move(start: .C2, end: .C4)) + try game.execute(move: Move(start: .C7, end: .C6)) + try game.execute(move: Move(start: .C4, end: .C5)) + try game.execute(move: Move(start: .D7, end: .D5)) + try game.execute(move: Move(start: .C5, end: .D6)) + #endif + } catch { + #if swift(>=3) + let str = String(describing: error) + #else + let str = String(error) + #endif + XCTFail(str) + } + } + + func testGameWhiteKingSideCastlingRightsAfterRookCapture() { + let startFen = "r3k2r/8/8/8/8/8/8/R3K2R b KQkq - 0 1" + let startPosition = Game.Position(fen: startFen)! + let game = try! Game(position: startPosition) + #if swift(>=3) + let move = Move(start: .h8, end: .h1) + #else + let move = Move(start: .H8, end: .H1) + #endif + try! game.execute(move: move) + #if swift(>=3) + XCTAssertFalse(game.castlingRights.contains(.whiteKingside)) + #else + XCTAssertFalse(game.castlingRights.contains(.WhiteKingside)) + #endif + } + + func testGameWhiteQueenSideCastlingRightsAfterRookCapture() { + let startFen = "r3k2r/8/8/8/8/8/8/R3K2R b KQkq - 0 1" + let startPosition = Game.Position(fen: startFen)! + let game = try! Game(position: startPosition) + #if swift(>=3) + let move = Move(start: .a8, end: .a1) + #else + let move = Move(start: .A8, end: .A1) + #endif + try! game.execute(move: move) + #if swift(>=3) + XCTAssertFalse(game.castlingRights.contains(.whiteQueenside)) + #else + XCTAssertFalse(game.castlingRights.contains(.WhiteQueenside)) + #endif + } + + func testGameBlackKingSideCastlingRightsAfterRookCapture() { + let startFen = "r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1" + let startPosition = Game.Position(fen: startFen)! + let game = try! Game(position: startPosition) + #if swift(>=3) + let move = Move(start: .h1, end: .h8) + #else + let move = Move(start: .H1, end: .H8) + #endif + try! game.execute(move: move) + #if swift(>=3) + XCTAssertFalse(game.castlingRights.contains(.blackKingside)) + #else + XCTAssertFalse(game.castlingRights.contains(.BlackKingside)) + #endif + } + + func testGameBlackQueenSideCastlingRightsAfterRookCapture() { + let startFen = "r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1" + let startPosition = Game.Position(fen: startFen)! + let game = try! Game(position: startPosition) + #if swift(>=3) + let move = Move(start: .a1, end: .a8) + #else + let move = Move(start: .A1, end: .A8) + #endif + try! game.execute(move: move) + #if swift(>=3) + XCTAssertFalse(game.castlingRights.contains(.blackQueenside)) + #else + XCTAssertFalse(game.castlingRights.contains(.BlackQueenside)) + #endif + } + + func testGameUndoAndRedo() throws { + let game = Game() + let startBoard = game.board + var endBoard = startBoard + var moves = [Move]() + + while let move = game.availableMoves().random() { + try game.execute(uncheckedMove: move) + moves.append(move) + endBoard = game.board + } + #if swift(>=3) + var redoMoves = moves.reversed() as [Move] + #else + var redoMoves = moves.reverse() as [Move] + #endif + + while let move = game.moveToUndo() { + XCTAssertEqual(move, game.undoMove()) + XCTAssertEqual(move, moves.popLast()) + } + XCTAssert(moves.isEmpty) + XCTAssertEqual(game.board, startBoard) + + while let move = game.moveToRedo() { + XCTAssertEqual(move, game.redoMove()) + XCTAssertEqual(move, redoMoves.popLast()) + } + XCTAssertEqual(game.board, endBoard) + } + + func testPGNParsingAndExporting() throws { + let immortalGame = String() + + "[Event \"London\"]\n" + + "[Site \"London\"]\n" + + "[Date \"1851.??.??\"]\n" + + "[EventDate \"?\"]\n" + + "[Round \"?\"]\n" + + "[Result \"1-0\"]\n" + + "[White \"Adolf Anderssen\"]\n" + + "[Black \"Kieseritzky\"]\n" + + "[ECO \"C33\"]\n" + + "[WhiteElo \"?\"]\n" + + "[BlackElo \"?\"]\n" + + "[PlyCount \"45\"]\n" + + "\n" + + "1.e4 e5 2.f4 exf4 3.Bc4 Qh4+ 4.Kf1 b5 5.Bxb5 Nf6 6.Nf3 Qh6 7.d3 Nh5 8.Nh4 Qg5\n" + + "9.Nf5 c6 10.g4 Nf6 11.Rg1 cxb5 12.h4 Qg6 13.h5 Qg5 14.Qf3 Ng8 15.Bxf4 Qf6\n" + + "16.Nc3 Bc5 17.Nd5 Qxb2 18.Bd6 Bxg1 19. e5 Qxa1+ 20. Ke2 Na6 21.Nxg7+ Kd8\n" + + "22.Qf6+ Nxf6 23.Be7# 1-0\n" + + let returnGame = String() + + "[Event \"F/S Return Match\"]\n" + + "[Site \"Belgrade, Serbia Yugoslavia|JUG\"]\n" + + "[Date \"1992.11.04\"]\n" + + "[Round \"29\"]\n" + + "[White \"Fischer, Robert J.\"]\n" + + "[Black \"Spassky, Boris V.\"]\n" + + "[Result \"1/2-1/2\"]\n" + + "\n" + + "1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 {This opening is called the Ruy Lopez.}\n" + + "4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Nb8 10. d4 Nbd7\n" + + "11. c4 c6 12. cxb5 axb5 13. Nc3 Bb7 14. Bg5 b4 15. Nb1 h6 16. Bh4 c5 17. dxe5\n" + + "Nxe4 18. Bxe7 Qxe7 19. exd6 Qf6 20. Nbd2 Nxd6 21. Nc4 Nxc4 22. Bxc4 Nb6\n" + + "23. Ne5 Rae8 24. Bxf7+ Rxf7 25. Nxf7 Rxe1+ 26. Qxe1 Kxf7 27. Qe3 Qg5 28. Qxg5\n" + + "hxg5 29. b3 Ke6 30. a3 Kd6 31. axb4 cxb4 32. Ra5 Nd5 33. f3 Bc8 34. Kf2 Bf5\n" + + "35. Ra7 g6 36. Ra6+ Kc5 37. Ke1 Nf4 38. g3 Nxh3 39. Kd2 Kb5 40. Rd6 Kc5 41. Ra6\n" + + "Nf2 42. g4 Bd3 43. Re6 1/2-1/2\n" + + let immortalGamePGN = try PGN(parse: immortalGame) + XCTAssertEqual(immortalGamePGN.moves.count, 45) + XCTAssertEqual(immortalGamePGN.outcome, Game.Outcome._win(._white)) + let returnGamePGN = try PGN(parse: returnGame) + XCTAssertEqual(returnGamePGN.moves.count, 85) + XCTAssertEqual(returnGamePGN.outcome, Game.Outcome._draw) + + let immortalGameExportedPGN = try PGN(parse: immortalGamePGN.exported()) + let returnGameExportedPGN = try PGN(parse: returnGamePGN.exported()) + XCTAssertEqual(immortalGameExportedPGN, immortalGamePGN) + XCTAssertEqual(returnGameExportedPGN, returnGamePGN) + XCTAssertNotEqual(immortalGameExportedPGN, returnGamePGN) + XCTAssertNotEqual(returnGameExportedPGN, immortalGamePGN) + } + + } + + extension Int { + + static func random(from value: Int) -> Int { + #if os(OSX) || os(iOS) || os(watchOS) || os(tvOS) + return Int(arc4random_uniform(UInt32(value))) + #elseif os(Linux) + srand(.init(time(nil))) + return Int(rand() % .init(value)) + #else + fatalError("Unknown OS") + #endif + } + + } + + extension Array { + + func random() -> Element? { + return !self.isEmpty ? self[.random(from: count)] : nil + } + + } + + extension Move { + + func formatted() -> String { + let result = ".\(start) >>> .\(end)" + #if swift(>=3) + return result.lowercased() + #else + return result.uppercaseString + #endif + } + + } + + extension Sequence where Iterator.Element == Move { + + func formatted() -> String { + let values = map { $0.formatted() } + let string = values.joined(separator: ", ") + return "[" + string + "]" + } + + } |