aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNavan Chauhan <navanchauhan@gmail.com>2024-04-17 12:00:05 -0600
committerNavan Chauhan <navanchauhan@gmail.com>2024-04-17 12:00:05 -0600
commit424fe090aa919d7ef70720d663bd280d09092bdf (patch)
treee8d54467b3f3cb77ed778d1fadbc826a2e6aab7a
initial commit
-rw-r--r--.gitignore8
-rw-r--r--Package.swift23
-rw-r--r--README.md27
m---------Sources0
-rw-r--r--Tests/SwiftChessNeoTests/PGNParsingTests.swift294
-rw-r--r--Tests/SwiftChessNeoTests/SwiftChessNeoTests.swift539
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 + "]"
+ }
+
+ }