From c36319d4d71210ae58ff6da04e5b95d89e4ae66e Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:08:25 +0200 Subject: [PATCH] batman --- .gitignore | 8 + .swift-format | 69 +++++++++ .vscode/launch.json | 24 +++ .vscode/settings.json | 1 + Package.swift | 25 ++++ Sources/Engine/Board.swift | 219 ++++++++++++++++++++++++++++ Sources/Engine/Pieces/Pawn.swift | 53 +++++++ Sources/Engine/Pieces/Piece.swift | 48 ++++++ Sources/Engine/Square.swift | 24 +++ Sources/Engine/utils.swift | 4 + Sources/exe/main.swift | 5 + Tests/EngineTests/EngineTests.swift | 15 ++ 12 files changed, 495 insertions(+) create mode 100644 .gitignore create mode 100644 .swift-format create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 Package.swift create mode 100644 Sources/Engine/Board.swift create mode 100644 Sources/Engine/Pieces/Pawn.swift create mode 100644 Sources/Engine/Pieces/Piece.swift create mode 100644 Sources/Engine/Square.swift create mode 100644 Sources/Engine/utils.swift create mode 100644 Sources/exe/main.swift create mode 100644 Tests/EngineTests/EngineTests.swift 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/.swift-format b/.swift-format new file mode 100644 index 0000000..d226c08 --- /dev/null +++ b/.swift-format @@ -0,0 +1,69 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentation" : { + "tabs" : 1 + }, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineLength" : 80, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : true, + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether" : false, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + }, + "spacesAroundRangeFormationOperators" : false, + "tabWidth" : 4, + "version" : 1 +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ff53deb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "configurations": [ + { + "type": "lldb", + "request": "launch", + "sourceLanguages": ["swift"], + "args": [], + "cwd": "${workspaceFolder:swift-chess}", + "name": "Debug exe", + "program": "${workspaceFolder:swift-chess}/.build/debug/exe", + "preLaunchTask": "swift: Build Debug exe" + }, + { + "type": "lldb", + "request": "launch", + "sourceLanguages": ["swift"], + "args": [], + "cwd": "${workspaceFolder:swift-chess}", + "name": "Release exe", + "program": "${workspaceFolder:swift-chess}/.build/release/exe", + "preLaunchTask": "swift: Build Release exe" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..2fd76fc --- /dev/null +++ b/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Engine", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "Engine", + targets: ["Engine"]), + .executable(name: "exe", targets: ["exe"]), + ], + 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: "Engine"), + .executableTarget(name: "exe", dependencies: ["Engine"]), + .testTarget( + name: "EngineTests", + dependencies: ["Engine"]), + ] +) diff --git a/Sources/Engine/Board.swift b/Sources/Engine/Board.swift new file mode 100644 index 0000000..fe28a0c --- /dev/null +++ b/Sources/Engine/Board.swift @@ -0,0 +1,219 @@ +public struct Fen: CustomStringConvertible { + public enum CastlingAvailibility: String { + case Neither = "-" + case WhiteKingSide = "K" + case WhiteQueenSide = "Q" + case BlackKingSide = "k" + case BlackQueenSide = "q" + case WhiteSide = "KQ" + case BlackSide = "kq" + case Kings = "Kk" + case Queens = "Qq" + case WKingBQueen = "Kq" + case WQueenBKing = "Qk" + case All = "KQkq" + } + private var _fen: String = "" + private var value: String { + get { + return _fen + } + set { + _fen = newValue + let splitted = _fen.split(separator: " ") + guard splitted.count == 6 else { + return + } + placement = String(splitted[0]) + activeColor = + switch splitted[1] { + case "w": .White + case "b": .Black + default: .White + } + castlingAvailibility = + CastlingAvailibility( + rawValue: + String(splitted[2])) ?? .Neither + enPassant = String(splitted[3]) + halfMoveClock = + if let c = splitted[4].first, c.isWholeNumber { + UInt8(String(c)) ?? 0 + } else { + 0 + } + fullMoveClock = + if let c = splitted[5].first, c.isWholeNumber { + UInt8(String(c)) ?? 1 + } else { + 1 + } + } + } + + public private(set) var placement: String = "" // 70 chars + public private(set) var activeColor: Color = .White // 1 char + public private(set) var castlingAvailibility: CastlingAvailibility = .All + // 1 to 4 chars + public private(set) var enPassant: String = "-" // 1 or 2 chars + public private(set) var halfMoveClock: UInt8 = 0 + public package(set) var fullMoveClock: UInt8 = 1 + + public init(fen value: String) { + self.value = value + } + + public enum FenError: Error { + case InvalidCharacter(c: String, column: UInt8) + case NumberTooBig(n: UInt8, column: UInt8) + case NumberTooSmall(n: UInt8, column: UInt8) + } + + public var description: String { + return value + } +} + +public class Board: CustomStringConvertible { + public typealias Grid = [[Square]] + + private enum UnicodeBar: String, CustomStringConvertible { + case VerticalBar = "│" + case TopHorizontalLine = "┌───┬───┬───┬───┬───┬───┬───┬───┐" + case MiddleHorizontalLine = " ├───┼───┼───┼───┼───┼───┼───┼───┤" + case BottomHorizonLine = " └───┴───┴───┴───┴───┴───┴───┴───┘" + + var description: String { + return self.rawValue + } + } + + private var squares = [Square]() + private 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 private(set) var fen = + Fen(fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + + public func setBoard() throws -> Grid { + var file: UInt8 = 1 + var rank: UInt8 = 8 + var index: UInt8 = 0 + var b: Grid = board + for c in fen.placement { + if c == "/" { + rank -= 1 + file = 0 + } else if c.isWholeNumber, let n = UInt8(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[rank, i-1]?.piece = nil + } + file -= 1 + } else if c.isASCII { + switch Kind[c] { + case .some(let (k, c)): + let piece: Piece = + switch k { + case .Pawn: + Pawn(color: c, on: .init(file: file, rank: rank)) + default: + Pawn(color: c, on: .init(file: file, rank: rank)) + } + b[rank, file-1]?.piece = piece + case .none: + throw Fen.FenError.InvalidCharacter( + c: String(c), column: index) + } + } + + file += 1 + index += 1 + } + + return b + } + + public required init() { + var rank: UInt8 = 8 + for i in 0...63 { + let index = UInt8(i) + let square = Square( + position: .init(file: (index % 8) + 1, rank: rank), + index: index, + color: index % 2 != rank % 2 ? .Black : .White) + squares.append(square) + if (index + 1) % 8 == 0 { + rank -= 1 + } + } + } + + public var description: String { + var boardString = + " A B C D E F G H \n \(UnicodeBar.TopHorizontalLine)\n" + var _rank: UInt8 = 8 + board.forEach { rank in + boardString += String(_rank) + " \(UnicodeBar.VerticalBar)" + rank.forEach { square in + let p = square.piece?.unicodeRepresentation ?? " " + boardString += " \(p) \(UnicodeBar.VerticalBar)" + } + boardString += "\n" + _rank -= 1 + if _rank > 0 { + boardString += "\(UnicodeBar.MiddleHorizontalLine)\n" + } + } + boardString += "\(UnicodeBar.BottomHorizonLine)" + return boardString + } + + public subscript(pos: Square.Position) -> Square? { + let i = pos.index + if i > squares.count { + return nil + } + return squares[i] + } +} + +extension Board.Grid { + public subscript(rank: UInt8, file: UInt8) -> Square? { + get { + guard 1 > rank && rank < 9 && 1 > file && file < 9 else { + return nil + } + + return self[(8-Int(rank)) % 8][Int(file) - 1] + } + set { + guard 1 > rank && rank < 9 && 1 > file && file < 9 else { + return + } + + guard let n = newValue else { + return + } + + self[(8-Int(rank)) % 8][Int(file) - 1] = n + } + } +} \ No newline at end of file diff --git a/Sources/Engine/Pieces/Pawn.swift b/Sources/Engine/Pieces/Pawn.swift new file mode 100644 index 0000000..b1b4b82 --- /dev/null +++ b/Sources/Engine/Pieces/Pawn.swift @@ -0,0 +1,53 @@ +package class Pawn: Piece { + package var kind: Kind = .Pawn + package weak var board: Board? + package var color: Color + package var position: Square.Position + package var pseudoLegalPositions: [Square.Position] { + return [] + } + package var legalPositions: [Square.Position] { + return pseudoLegalPositions.filter { isLegal(pos: $0) } + } + + package var unicodeRepresentation: String { + return color == .Black ? "♟" : "♙" + } + + package func move(dst: Square.Position) -> Bool { + guard board != nil else { + return false + } + + if !(legalPositions.contains { $0 == dst }) { + return false + } + + if let board = board, var s = board[position], var d = board[dst] { + s.piece = self + d.piece = nil + } + + position = dst + + return true + } + + package func isLegal(pos: Square.Position) -> Bool { + // TODO: Handle "En-Passant" + if let board = board, let s = board[pos] { + if let p = s.piece { + if p.color == color { return false } + } + + } + return true + } + + package init( + color: Color, on position: Square.Position + ) { + self.color = color + self.position = position + } +} diff --git a/Sources/Engine/Pieces/Piece.swift b/Sources/Engine/Pieces/Piece.swift new file mode 100644 index 0000000..12df6fa --- /dev/null +++ b/Sources/Engine/Pieces/Piece.swift @@ -0,0 +1,48 @@ +public enum Kind: String, CaseIterable { + case Pawn, Knight, Bishop, Rook, Queen, King + + public var value: Int8 { + switch self { + case .Pawn: 1 + case .Bishop: 3 + case .Knight: 3 + case .Rook: 5 + case .Queen: 9 + case .King: -1 + } + } + + + public static subscript(_ c: Character) -> (Self, Color)? { + let v = c.uppercased() + + guard v == "N" || (Self.allCases.contains { String($0.rawValue.first!) == v}) else { + return nil + } + + let kind: Self = switch v { + case "P": .Pawn + case "N": .Knight + case "B": .Bishop + case "R": .Rook + case "Q": .Queen + case "K": .King + default: .Pawn + } + + let color: Color = c.isUppercase ? .White : .Black + + return (kind, color) + } +} + +public protocol Piece { + var color: Color { get } + var unicodeRepresentation: String { get } + var kind: Kind { get } + var position: Square.Position { get } + var pseudoLegalPositions: [Square.Position] { get } + var legalPositions: [Square.Position] { get } + func move(dst: Square.Position) -> Bool + func isLegal(pos: Square.Position) -> Bool +} diff --git a/Sources/Engine/Square.swift b/Sources/Engine/Square.swift new file mode 100644 index 0000000..065ab9e --- /dev/null +++ b/Sources/Engine/Square.swift @@ -0,0 +1,24 @@ +public struct Square: Equatable { + + public struct Position: Equatable { + public let file: UInt8 + public let rank: UInt8 + + public var index: Int { + return Int(file * rank) - 1 + } + + public static func == (lhs: Position, rhs: Position) -> Bool { + return lhs.rank == rhs.rank && lhs.file == rhs.file + } + } + + public let position: Position + public let index: UInt8 + public var piece: Piece? = nil + public let color: Color + + public static func == (lhs: Square, rhs: Square) -> Bool { + return lhs.position == rhs.position + } +} diff --git a/Sources/Engine/utils.swift b/Sources/Engine/utils.swift new file mode 100644 index 0000000..d15222a --- /dev/null +++ b/Sources/Engine/utils.swift @@ -0,0 +1,4 @@ +public enum Color: UInt8 { + case Black = 0 + case White = 1 +} diff --git a/Sources/exe/main.swift b/Sources/exe/main.swift new file mode 100644 index 0000000..718102c --- /dev/null +++ b/Sources/exe/main.swift @@ -0,0 +1,5 @@ +import Engine + +let board = Board() +print(try! board.setBoard()) + diff --git a/Tests/EngineTests/EngineTests.swift b/Tests/EngineTests/EngineTests.swift new file mode 100644 index 0000000..5b591cb --- /dev/null +++ b/Tests/EngineTests/EngineTests.swift @@ -0,0 +1,15 @@ +import XCTest + +@testable import Engine + +final class EngineTests: XCTestCase { + override func setUp() { + super.setUp() + } + // func testBoard() throws { + // let board = Board() + // } + override func tearDown() { + super.tearDown() + } +}