20 Commits

Author SHA1 Message Date
Steve Barnegren 29cb7535a4 Added init method for ascii board 2017-02-01 08:08:30 +00:00
Steve Barnegren 97b10dfa7b AI can play opening moves (although currently crashing) 2017-01-31 22:04:21 +00:00
Steve Barnegren b3ab7cfbf0 Break out openings in to separate moves and board states 2017-01-31 21:26:13 +00:00
Steve Barnegren 82f68a6875 Added moves for some common openings 2017-01-31 20:19:25 +00:00
Steve Barnegren c0cc240716 Added tests for board location 2017-01-31 18:26:33 +00:00
Steve Barnegren 8c64655149 Added GridPositon to BoardLocation 2017-01-31 18:01:32 +00:00
Steve Barnegren ee073d5edf Added board method to be able to get possible moves locations for a piece 2017-01-30 21:44:20 +00:00
Steve Barnegren 8592eb4928 Use forEach to make a few verbose loops more concise 2017-01-28 10:57:21 +00:00
Steve Barnegren 1594973694 Added game state enum to game 2017-01-26 20:38:57 +00:00
Steve Barnegren 8c1ad9b6e5 Update readme 2017-01-17 18:12:15 +00:00
Steve Barnegren babc1cf062 Updated podspec 2017-01-12 21:22:17 +00:00
Steve Barnegren dde1d3f054 Slightly adjusted AI Configuration values 2017-01-12 20:28:03 +00:00
Steve Barnegren e93f6758fc Added king's gambit opening 2017-01-11 20:08:16 +00:00
Steve Barnegren aaec289f33 Made ASCIIBoard part of SwiftChess module 2017-01-09 21:35:27 +00:00
Steve Barnegren 8fd84bea84 Implemented en passant 2017-01-09 21:12:40 +00:00
Steve Barnegren fa2e34d026 Added logic for whether piece is able to be taken via en passant move 2017-01-08 20:13:10 +00:00
Steve Barnegren 8fa6ca0002 Added failing tests for en passant rule 2017-01-08 19:47:43 +00:00
Steve Barnegren b200acaffb Fix issue with pawns not being able to check king 2017-01-08 18:37:18 +00:00
Steve Barnegren 150e763d87 Added behavioural test 2017-01-08 11:07:11 +00:00
Steve Barnegren 5503a770b6 Altered configuration weightings 2017-01-06 08:36:43 +00:00
23 changed files with 1954 additions and 316 deletions
+12 -4
View File
@@ -13,6 +13,7 @@
6719898D1DFFE0F40053EA3D /* BoardRaterBoardDominanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6719898C1DFFE0F40053EA3D /* BoardRaterBoardDominanceTests.swift */; };
671989911DFFE8650053EA3D /* BoardRaterCenterDominanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671989901DFFE8650053EA3D /* BoardRaterCenterDominanceTests.swift */; };
676EF7C51E15AC1700E275B4 /* BoardRaterKingSurroundingPossession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 676EF7C41E15AC1700E275B4 /* BoardRaterKingSurroundingPossession.swift */; };
67A3EB161E3A926C00F6F01B /* BoardScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67A3EB121E3A826800F6F01B /* BoardScenarios.swift */; };
67A9CA341DE64DD600510FB8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
67A9CA351DE64DD800510FB8 /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* MenuViewController.swift */; };
67A9CA361DE64DDA00510FB8 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67AD33A61D7C67BF002730DF /* GameViewController.swift */; };
@@ -24,7 +25,6 @@
67A9CA3D1DE64E7100510FB8 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 607FACEA1AFB9204008FA782 /* Info.plist */; };
67B73A9B1E15351900C19176 /* PromotionSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B73A9A1E15351900C19176 /* PromotionSelectionViewController.swift */; };
67B73A9F1E154C1E00C19176 /* AIPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B73A9E1E154C1E00C19176 /* AIPlayerTests.swift */; };
67D54A5C1DE7682900C12258 /* ASCIIBoard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67D54A501DE7680E00C12258 /* ASCIIBoard.swift */; };
67D54A5D1DE7682D00C12258 /* BoardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67D54A511DE7680E00C12258 /* BoardTests.swift */; };
67D54A5E1DE7683000C12258 /* GameTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67D54A521DE7680E00C12258 /* GameTests.swift */; };
67D54A5F1DE7683300C12258 /* PieceMovementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67D54A531DE7680E00C12258 /* PieceMovementTests.swift */; };
@@ -37,6 +37,8 @@
67F779261E1C32A400885B89 /* BoardRaterCenterFourOccupationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F779251E1C32A400885B89 /* BoardRaterCenterFourOccupationTests.swift */; };
67F9DB6E1E1AC8DC00C7EC5A /* BoardRaterCheckMateOpportunityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F9DB6D1E1AC8DC00C7EC5A /* BoardRaterCheckMateOpportunityTests.swift */; };
67F9DB751E1AD3BB00C7EC5A /* AIBehaviourTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F9DB741E1AD3BB00C7EC5A /* AIBehaviourTests.swift */; };
67FD868D1E41099B0023335C /* BoardLocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67FD868C1E41099B0023335C /* BoardLocationTests.swift */; };
67FD86911E4128F00023335C /* OpeningsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67FD86901E4128F00023335C /* OpeningsTests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -77,6 +79,7 @@
6719898C1DFFE0F40053EA3D /* BoardRaterBoardDominanceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardRaterBoardDominanceTests.swift; sourceTree = "<group>"; };
671989901DFFE8650053EA3D /* BoardRaterCenterDominanceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardRaterCenterDominanceTests.swift; sourceTree = "<group>"; };
676EF7C41E15AC1700E275B4 /* BoardRaterKingSurroundingPossession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardRaterKingSurroundingPossession.swift; sourceTree = "<group>"; };
67A3EB121E3A826800F6F01B /* BoardScenarios.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardScenarios.swift; sourceTree = "<group>"; };
67A9CA071DE64D6500510FB8 /* SwiftChess.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SwiftChess.xcodeproj; path = ../SwiftChess/SwiftChess.xcodeproj; sourceTree = "<group>"; };
67A9CA141DE64DAA00510FB8 /* SwiftChessExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftChessExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
67A9CA271DE64DAA00510FB8 /* SwiftChessExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftChessExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -84,7 +87,6 @@
67AD33A81D7C67DF002730DF /* BoardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardView.swift; sourceTree = "<group>"; };
67B73A9A1E15351900C19176 /* PromotionSelectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromotionSelectionViewController.swift; sourceTree = "<group>"; };
67B73A9E1E154C1E00C19176 /* AIPlayerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AIPlayerTests.swift; sourceTree = "<group>"; };
67D54A501DE7680E00C12258 /* ASCIIBoard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ASCIIBoard.swift; sourceTree = "<group>"; };
67D54A511DE7680E00C12258 /* BoardTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardTests.swift; sourceTree = "<group>"; };
67D54A521DE7680E00C12258 /* GameTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameTests.swift; sourceTree = "<group>"; };
67D54A531DE7680E00C12258 /* PieceMovementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieceMovementTests.swift; sourceTree = "<group>"; };
@@ -97,6 +99,8 @@
67F779251E1C32A400885B89 /* BoardRaterCenterFourOccupationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardRaterCenterFourOccupationTests.swift; sourceTree = "<group>"; };
67F9DB6D1E1AC8DC00C7EC5A /* BoardRaterCheckMateOpportunityTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardRaterCheckMateOpportunityTests.swift; sourceTree = "<group>"; };
67F9DB741E1AD3BB00C7EC5A /* AIBehaviourTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AIBehaviourTests.swift; sourceTree = "<group>"; };
67FD868C1E41099B0023335C /* BoardLocationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardLocationTests.swift; sourceTree = "<group>"; };
67FD86901E4128F00023335C /* OpeningsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpeningsTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -173,6 +177,7 @@
children = (
607FACE91AFB9204008FA782 /* Supporting Files */,
67D54A651DE986F700C12258 /* PieceTests.swift */,
67FD868C1E41099B0023335C /* BoardLocationTests.swift */,
67D54A511DE7680E00C12258 /* BoardTests.swift */,
67D54A521DE7680E00C12258 /* GameTests.swift */,
67D54A531DE7680E00C12258 /* PieceMovementTests.swift */,
@@ -180,6 +185,7 @@
67B73A9E1E154C1E00C19176 /* AIPlayerTests.swift */,
67F7791F1E1B923B00885B89 /* AIConfigurationTests.swift */,
67F9DB741E1AD3BB00C7EC5A /* AIBehaviourTests.swift */,
67FD86901E4128F00023335C /* OpeningsTests.swift */,
67D54A621DE9768200C12258 /* BoardRaters */,
67D54A551DE7680E00C12258 /* Tests.swift */,
);
@@ -189,8 +195,8 @@
607FACE91AFB9204008FA782 /* Supporting Files */ = {
isa = PBXGroup;
children = (
67D54A501DE7680E00C12258 /* ASCIIBoard.swift */,
607FACEA1AFB9204008FA782 /* Info.plist */,
67A3EB121E3A826800F6F01B /* BoardScenarios.swift */,
);
name = "Supporting Files";
sourceTree = "<group>";
@@ -372,8 +378,10 @@
buildActionMask = 2147483647;
files = (
67D54A601DE7683800C12258 /* PlayerTests.swift in Sources */,
67FD86911E4128F00023335C /* OpeningsTests.swift in Sources */,
67D54A611DE7683A00C12258 /* Tests.swift in Sources */,
676EF7C51E15AC1700E275B4 /* BoardRaterKingSurroundingPossession.swift in Sources */,
67FD868D1E41099B0023335C /* BoardLocationTests.swift in Sources */,
67D54A5D1DE7682D00C12258 /* BoardTests.swift in Sources */,
67F9DB751E1AD3BB00C7EC5A /* AIBehaviourTests.swift in Sources */,
671989911DFFE8650053EA3D /* BoardRaterCenterDominanceTests.swift in Sources */,
@@ -382,9 +390,9 @@
67D54A661DE986F700C12258 /* PieceTests.swift in Sources */,
671989891DFFE0410053EA3D /* BoardRaterCenterOwnershipTests.swift in Sources */,
67B73A9F1E154C1E00C19176 /* AIPlayerTests.swift in Sources */,
67D54A5C1DE7682900C12258 /* ASCIIBoard.swift in Sources */,
67F779201E1B923B00885B89 /* AIConfigurationTests.swift in Sources */,
67D54A641DE976A900C12258 /* BoardRaterCountPiecesTests.swift in Sources */,
67A3EB161E3A926C00F6F01B /* BoardScenarios.swift in Sources */,
09AE32551E03D71D00A149FE /* BoardRaterPawnProgressionTests.swift in Sources */,
09A4C0291E013ECB000CFBF4 /* BoardRaterThreatenedPiecesTests.swift in Sources */,
67D54A5F1DE7683300C12258 /* PieceMovementTests.swift in Sources */,
+147
View File
@@ -6,6 +6,12 @@
// Copyright © 2017 CocoaPods. All rights reserved.
//
/*
AI behaviour tests are tests that try to avoid or encourage certain behaviours in the AI.
These are complex outcomes, so may require changing several variables to manipulate outcomes.
*/
import XCTest
@testable import SwiftChess
@@ -21,7 +27,148 @@ class AIBehaviourTests: XCTestCase {
super.tearDown()
}
// MARK: - Helpers
func makeGameWithBoard(board: Board, colorToMove: Color) -> Game {
let whitePlayer = AIPlayer(color: .white)
let blackPlayer = AIPlayer(color: .black)
let game = Game(firstPlayer: whitePlayer, secondPlayer: blackPlayer, board: board, colorToMove: colorToMove)
return game
}
func findMovedPieceLocation(startBoard: Board, endBoard: Board, color: Color) -> BoardLocation {
for location in BoardLocation.all {
let startBoardPiece = startBoard.getPiece(at: location)
let endBoardPiece = endBoard.getPiece(at: location)
// If there is no end board piece, this is not the location
if endBoardPiece == nil {
continue
}
// If the end piece exists, but start doesn't, it's the moved piece
if endBoardPiece != nil && startBoardPiece == nil {
return location
}
// If both pieces exist, and are not the same, then it's the location
if endBoardPiece != nil && startBoardPiece != nil {
if endBoardPiece!.color != startBoardPiece!.color
&& endBoardPiece!.type != startBoardPiece!.type{
return location
}
}
// Else continue - this isn't the location
continue
}
fatalError("Failed to find moved location")
}
// MARK: - Scenario Tests
func test_ScenarioOne_BlackShouldNotGiveAwayBishop() {
// In the following example, the black player can move the bishop at (5,7) to the * location (2,4).
// The can look like a good move because the bishop will then threaten the white queen at (3,3).
// The white queen will also be threatening the black bishop, but because this is a lower value piece black can think that it is in a more preferrable position.
// In reality, because the black bishop is unprotected, the white queen will take it.
let board = ASCIIBoard(pieces: "r - b - q b - r" +
"p p g - - p p p" +
"- - - - - k - -" +
"- - * - - - - -" +
"- - p Q P - - -" +
"- - - - - - - -" +
"P P G - - P P P" +
"R K B - - B K R" )
let location = board .locationOfCharacter("*")
let game = makeGameWithBoard(board: board.board, colorToMove: .black)
guard let player = game.currentPlayer as? AIPlayer else {
XCTFail("Expected an AI Player")
return
}
player.makeMove()
// If there is not piece at the location, then test has passed
guard let piece = game.board.getPiece(at: location) else {
return
}
// If the piece is not black, or is not a bishop, test passed
if piece.type != .bishop || piece.color != .black {
return
}
// Otherwise, black has moved the bishop to the *, test failed
XCTFail("Black moved bishop")
}
func test_ScenarioTwo_BlackShouldTradeKnight() {
// In this example, the black knight at (2, 1) can either take the white rook at (0,0), or trade itself for the white queen at (0,2)
// Both of these are good moves
let board = ASCIIBoard(pieces: "- r - - q b k r" +
"p - - g - - p p" +
"- - - - b p - -" +
"- - - - p - - -" +
"- - - p P - - -" +
"Q - - - - - - -" +
"P P k P K P P P" +
"R K B G - - - R" )
let queenLocation = BoardLocation(x: 0, y: 2)
let rookLocation = BoardLocation(x: 0, y: 0)
let game = makeGameWithBoard(board: board.board, colorToMove: .black)
guard let player = game.currentPlayer as? AIPlayer else {
XCTFail("Expected an AI Player")
return
}
player.makeMove()
if let queenLocationPiece = game.board.getPiece(at: queenLocation) {
if queenLocationPiece.color == .black && queenLocationPiece.type == .knight {
print("PASSED - Black took queen")
return
}
}
if let rookLocationPiece = game.board.getPiece(at: rookLocation) {
if rookLocationPiece.color == .black && rookLocationPiece.type == .knight {
print("PASSED - black took rook")
return
}
}
let location = findMovedPieceLocation(startBoard: board.board, endBoard: game.board, color: .black)
guard let piece = game.board.getPiece(at: location) else {
XCTFail("Couldn't find moved piece")
return
}
XCTFail("FAILED - Black moved \(piece.type) to (\(location.x),\(location.y))")
}
+157
View File
@@ -32,4 +32,161 @@ class AIPlayerTests: XCTestCase {
}
}
// MARK: - Test Cannot move in the check
func makeTestGame(board: Board, colorToMove: Color) -> Game {
let whitePlayer = AIPlayer(color: .white)
let blackPlayer = AIPlayer(color: .black)
let game = Game(firstPlayer: whitePlayer, secondPlayer: blackPlayer, board: board, colorToMove: colorToMove)
return game
}
func testKnightCannotPutOwnKingInToCheck() {
let board = ASCIIBoard(pieces: "r - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - * - - - - -" +
"K - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"G - - - - - - -" )
let knightLocation = board.locationOfCharacter("K")
let testLocation = board.locationOfCharacter("*")
let game = makeTestGame(board: board.board, colorToMove: .white)
guard let player = game.currentPlayer as? AIPlayer else {
fatalError()
}
XCTAssertFalse(player.canAIMovePiece(fromLocation: knightLocation, toLocation: testLocation))
}
func testKingCannotMoveInToCheck() {
// This is a complex scenario because it was one that I observed from an actual game
let board = ASCIIBoard(pieces: "r k - - q b - r" +
"p p p g k - - p" +
"- - * p b p - -" +
"P P - - p - p P" +
"- - P - P - - -" +
"- B - - Q P - -" +
"- B - P K G P -" +
"R K - - - - - R" )
let kingLocation = board.locationOfCharacter("g")
let testLocation = board.locationOfCharacter("*")
let game = makeTestGame(board: board.board, colorToMove: .black)
guard let player = game.currentPlayer as? AIPlayer else {
fatalError()
}
XCTAssertFalse(player.canAIMovePiece(fromLocation: kingLocation, toLocation: testLocation))
}
func testPawnCannotPutOwnKingInToCheck() {
let board = ASCIIBoard(pieces: "- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - * - - - -" +
"G - - P - - - r" +
"- - - - - - - -" )
let pawnLocation = board.locationOfCharacter("P")
let testLocation = board.locationOfCharacter("*")
let game = makeTestGame(board: board.board, colorToMove: .white)
guard let player = game.currentPlayer as? AIPlayer else {
fatalError()
}
XCTAssertFalse(player.canAIMovePiece(fromLocation: pawnLocation, toLocation: testLocation))
}
func testQueenCannotPutOwnKingInToCheck() {
let board = ASCIIBoard(pieces: "- - - * - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"G - - Q - - - r" +
"- - - - - - - -" )
let queenLocation = board.locationOfCharacter("Q")
let testLocation = board.locationOfCharacter("*")
let game = makeTestGame(board: board.board, colorToMove: .white)
guard let player = game.currentPlayer as? AIPlayer else {
fatalError()
}
XCTAssertFalse(player.canAIMovePiece(fromLocation: queenLocation, toLocation: testLocation))
}
func testRookCannotPutOwnKingInToCheck() {
let board = ASCIIBoard(pieces: "- - - * - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"G - - R - - - r" +
"- - - - - - - -" )
let rookLocation = board.locationOfCharacter("R")
let testLocation = board.locationOfCharacter("*")
let game = makeTestGame(board: board.board, colorToMove: .white)
guard let player = game.currentPlayer as? AIPlayer else {
fatalError()
}
XCTAssertFalse(player.canAIMovePiece(fromLocation: rookLocation, toLocation: testLocation))
}
func testBishopCannotPutOwnKingInToCheck() {
let board = ASCIIBoard(pieces: "- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - *" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"G - - B - - - r" +
"- - - - - - - -" )
let bishopLocation = board.locationOfCharacter("B")
let testLocation = board.locationOfCharacter("*")
let game = makeTestGame(board: board.board, colorToMove: .white)
guard let player = game.currentPlayer as? AIPlayer else {
fatalError()
}
XCTAssertFalse(player.canAIMovePiece(fromLocation: bishopLocation, toLocation: testLocation))
}
}
+161
View File
@@ -0,0 +1,161 @@
//
// BoardLocationTests.swift
// Example
//
// Created by Steve Barnegren on 31/01/2017.
// Copyright © 2017 CocoaPods. All rights reserved.
//
import XCTest
@testable import SwiftChess
class BoardLocationTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testBoardLocationFromGridPositionResultsInCorrectIndex() {
let testCases = [
// row 1
(BoardLocation.GridPosition.a1, 0),
(BoardLocation.GridPosition.b1, 1),
(BoardLocation.GridPosition.c1, 2),
(BoardLocation.GridPosition.d1, 3),
(BoardLocation.GridPosition.e1, 4),
(BoardLocation.GridPosition.f1, 5),
(BoardLocation.GridPosition.g1, 6),
(BoardLocation.GridPosition.h1, 7),
// row 2
(BoardLocation.GridPosition.a2, 8),
(BoardLocation.GridPosition.b2, 9),
(BoardLocation.GridPosition.c2, 10),
(BoardLocation.GridPosition.d2, 11),
(BoardLocation.GridPosition.e2, 12),
(BoardLocation.GridPosition.f2, 13),
(BoardLocation.GridPosition.g2, 14),
(BoardLocation.GridPosition.h2, 15),
// row 3
(BoardLocation.GridPosition.a3, 16),
(BoardLocation.GridPosition.b3, 17),
(BoardLocation.GridPosition.c3, 18),
(BoardLocation.GridPosition.d3, 19),
(BoardLocation.GridPosition.e3, 20),
(BoardLocation.GridPosition.f3, 21),
(BoardLocation.GridPosition.g3, 22),
(BoardLocation.GridPosition.h3, 23),
// row 4
(BoardLocation.GridPosition.a4, 24),
(BoardLocation.GridPosition.b4, 25),
(BoardLocation.GridPosition.c4, 26),
(BoardLocation.GridPosition.d4, 27),
(BoardLocation.GridPosition.e4, 28),
(BoardLocation.GridPosition.f4, 29),
(BoardLocation.GridPosition.g4, 30),
(BoardLocation.GridPosition.h4, 31),
// row 5
(BoardLocation.GridPosition.a5, 32),
(BoardLocation.GridPosition.b5, 33),
(BoardLocation.GridPosition.c5, 34),
(BoardLocation.GridPosition.d5, 35),
(BoardLocation.GridPosition.e5, 36),
(BoardLocation.GridPosition.f5, 37),
(BoardLocation.GridPosition.g5, 38),
(BoardLocation.GridPosition.h5, 39),
// row 6
(BoardLocation.GridPosition.a6, 40),
(BoardLocation.GridPosition.b6, 41),
(BoardLocation.GridPosition.c6, 42),
(BoardLocation.GridPosition.d6, 43),
(BoardLocation.GridPosition.e6, 44),
(BoardLocation.GridPosition.f6, 45),
(BoardLocation.GridPosition.g6, 46),
(BoardLocation.GridPosition.h6, 47),
// row 7
(BoardLocation.GridPosition.a7, 48),
(BoardLocation.GridPosition.b7, 49),
(BoardLocation.GridPosition.c7, 50),
(BoardLocation.GridPosition.d7, 51),
(BoardLocation.GridPosition.e7, 52),
(BoardLocation.GridPosition.f7, 53),
(BoardLocation.GridPosition.g7, 54),
(BoardLocation.GridPosition.h7, 55),
// row 8
(BoardLocation.GridPosition.a8, 56),
(BoardLocation.GridPosition.b8, 57),
(BoardLocation.GridPosition.c8, 58),
(BoardLocation.GridPosition.d8, 59),
(BoardLocation.GridPosition.e8, 60),
(BoardLocation.GridPosition.f8, 61),
(BoardLocation.GridPosition.g8, 62),
(BoardLocation.GridPosition.h8, 63),
]
for (grid, index) in testCases {
XCTAssertEqual(BoardLocation(gridPosition: grid).index, index)
XCTAssertEqual(BoardLocation(index: index).gridPosition, grid)
}
}
func testMoveLocationsForColorReturnsCorrectLocations() {
class FakeOpening : Opening {
override func moveGridPositions() -> [(fromPosition: BoardLocation.GridPosition, toPosition: BoardLocation.GridPosition)] {
let moves: [(BoardLocation.GridPosition, BoardLocation.GridPosition)] = [
(.e2, .e4), // white moves pawn to e4
(.e7, .e5), // black moves pawn to e5
(.g1, .f3), // white moves knight to f3
(.b8, .c6), // black moves knight to c6
(.f1, .b5), // white moves bishop to b5
]
return moves
}
}
let expectedWhiteLocations: [(fromLocation: BoardLocation, toLocation: BoardLocation)] = [
(BoardLocation(gridPosition: .e2), BoardLocation(gridPosition: .e4)),
(BoardLocation(gridPosition: .g1), BoardLocation(gridPosition: .f3)),
(BoardLocation(gridPosition: .f1), BoardLocation(gridPosition: .b5)),
]
let expectedBlackLocations: [(fromLocation: BoardLocation, toLocation: BoardLocation)] = [
(BoardLocation(gridPosition: .e7), BoardLocation(gridPosition: .e5)),
(BoardLocation(gridPosition: .b8), BoardLocation(gridPosition: .c6)),
]
let opening = FakeOpening()
let whiteMoves = opening.moves(forColor: .white)
XCTAssertEqual(whiteMoves.count, expectedWhiteLocations.count)
let blackMoves = opening.moves(forColor: .black)
XCTAssertEqual(blackMoves.count, expectedBlackLocations.count)
for i in 0..<whiteMoves.count {
let expected = expectedWhiteLocations[i]
let actual = whiteMoves[i]
XCTAssertEqual(expected.fromLocation, actual.fromLocation)
XCTAssertEqual(expected.toLocation, actual.toLocation)
}
for i in 0..<blackMoves.count {
let expected = expectedBlackLocations[i]
let actual = blackMoves[i]
XCTAssertEqual(expected.fromLocation, actual.fromLocation)
XCTAssertEqual(expected.toLocation, actual.toLocation)
}
}
}
+64
View File
@@ -0,0 +1,64 @@
//
// BoardScenarios.swift
// Example
//
// Created by Steve Barnegren on 26/01/2017.
// Copyright © 2017 CocoaPods. All rights reserved.
//
import Foundation
import SwiftChess
extension Board {
public static func whiteInStaleMateScenario() -> Board {
return ASCIIBoard(pieces: "- - r - r - - g" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"q - - - - - - -" +
"- - - G - - - -" ).board
}
public static func blackInStaleMateScenario() -> Board {
return ASCIIBoard(pieces: "- - R - R - - G" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"Q - - - - - - -" +
"- - - g - - - -" ).board
}
public static func whiteInCheckMateScenario() -> Board {
return ASCIIBoard(pieces: "- p g - - - - K" +
"- - - - - P - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"r - - - - - - -" +
"r - - G - - - -" ).board
}
public static func blackInCheckMateScenario() -> Board {
return ASCIIBoard(pieces: "- - - g - - - R" +
"- - - - - - - R" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- G - - - - - -" ).board
}
}
+67 -37
View File
@@ -29,10 +29,9 @@ class BoardTests: XCTestCase {
let board = Board(state: .empty)
for index in 0..<64 {
let piece = board.getPiece(at: BoardLocation(index: index))
XCTAssert(piece == nil, "Expected piece at index \(index) to be nil")
(0..<64).forEach {
let piece = board.getPiece(at: BoardLocation(index: $0))
XCTAssert(piece == nil, "Expected piece at index \($0) to be nil")
}
}
@@ -96,6 +95,29 @@ class BoardTests: XCTestCase {
}
}
// MARK: - Board Location
func testBoardLocationStrideToReturnsExpectedStride() {
let fromLocation = BoardLocation(x: 1, y: 1)
let toLocation = BoardLocation(x: 3, y: 4)
let stride = fromLocation.strideTo(location: toLocation)
XCTAssertEqual(stride.x, 2)
XCTAssertEqual(stride.y, 3)
}
func testBoardLocationStrideFromReturnsExpectedStride() {
let fromLocation = BoardLocation(x: 1, y: 1)
let toLocation = BoardLocation(x: 3, y: 4)
let stride = toLocation.strideFrom(location: fromLocation)
XCTAssertEqual(stride.x, 2)
XCTAssertEqual(stride.y, 3)
}
// MARK: - Piece Manipulation
func testSetAndGetPiece() {
@@ -145,8 +167,8 @@ class BoardTests: XCTestCase {
let blackPieces = board.getPieces(color: .black)
let allPieces = whitePieces + blackPieces
for piece in allPieces {
XCTAssertFalse(piece.hasMoved)
allPieces.forEach{
XCTAssertFalse($0.hasMoved)
}
}
@@ -158,12 +180,12 @@ class BoardTests: XCTestCase {
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"* - - - - - - -" +
"W - - - - - - -" +
"- - - - - - - -" )
let fromLocation = BoardLocation(index: board.indexOfCharacter("W") )
let toLocation = BoardLocation(x: 4, y: 4)
let fromLocation = board.locationOfCharacter("W")
let toLocation = board.locationOfCharacter("*")
var gameBoard = board.board
@@ -250,44 +272,27 @@ class BoardTests: XCTestCase {
var board = Board(state: .empty)
for location in whiteLocations {
board.setPiece(Piece(type: .pawn, color: .white), at: location)
whiteLocations.forEach{
board.setPiece(Piece(type: .pawn, color: .white), at: $0)
}
for location in blackLocations {
board.setPiece(Piece(type: .pawn, color: .black), at: location)
blackLocations.forEach{
board.setPiece(Piece(type: .pawn, color: .black), at: $0)
}
let returnedWhitelocations = board.getLocationsOfColor(.white)
XCTAssert(returnedWhitelocations.count == whiteLocations.count,
"Expected white count to be \(whiteLocations.count), was \(returnedWhitelocations.count)")
XCTAssertEqual(whiteLocations.count, returnedWhitelocations.count);
let returnedBlacklocations = board.getLocationsOfColor(.black)
XCTAssert(returnedBlacklocations.count == blackLocations.count,
"Expected white count to be \(blackLocations.count), was \(returnedBlacklocations.count)")
XCTAssertEqual(blackLocations.count, returnedBlacklocations.count);
for location in whiteLocations {
XCTAssert(
returnedWhitelocations.filter({ (returnedLocation: BoardLocation) -> Bool in
return location == returnedLocation
}).count == 1,
"Expected only one location to exist"
)
whiteLocations.forEach { (location) in
XCTAssertEqual(returnedWhitelocations.filter{ location == $0 }.count, 1)
}
for location in blackLocations {
XCTAssert(
returnedBlacklocations.filter({ (returnedLocation: BoardLocation) -> Bool in
return location == returnedLocation
}).count == 1,
"Expected only one location to exist"
)
blackLocations.forEach { (location) in
XCTAssertEqual(returnedBlacklocations.filter{ location == $0 }.count, 1)
}
}
func testIsColorInCheckReturnsTrueWhenInCheck() {
@@ -980,7 +985,32 @@ class BoardTests: XCTestCase {
XCTAssertFalse(board.doesColorOccupyLocation(color: .white, location: location))
XCTAssertFalse(board.doesColorOccupyLocation(color: .black, location: location))
}
// MARK: Test Possible Move Locations
func testPossibleMoveLocationsReturnsExpectedLocations(){
let asciiBoard = ASCIIBoard(pieces: "- - - - - - - -" +
"- - * - * - - -" +
"- * - - - * - -" +
"- - - K - - - -" +
"- * - - - * - -" +
"- - * - * - - -" +
"- - - - - - - -" +
"- - - - - - - -" );
let board = asciiBoard.board;
let sourceLocation = asciiBoard.locationOfCharacter("K")
let locations = board.possibleMoveLocationsForPiece(atLocation: sourceLocation)
XCTAssertEqual(locations.count, 8)
let expectedLocations = asciiBoard.locationsWithCharacter("*")
expectedLocations.forEach{ expectedLocation in
XCTAssertEqual(locations.filter( { $0 == expectedLocation} ).count, 1)
}
}
}
+85 -18
View File
@@ -7,20 +7,22 @@
//
import XCTest
import SwiftChess
@testable import SwiftChess
class GameTests: XCTestCase {
var whitePlayer: Human!
var blackPlayer: Human!
var game: Game!
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
let firstPlayer = Human(color: .white)
let secondPlayer = Human(color: .black)
whitePlayer = Human(color: .white)
blackPlayer = Human(color: .black)
game = Game(firstPlayer: firstPlayer, secondPlayer: secondPlayer)
game = Game(firstPlayer: whitePlayer, secondPlayer: blackPlayer)
}
override func tearDown() {
@@ -28,20 +30,85 @@ class GameTests: XCTestCase {
super.tearDown()
}
/*
func testCurrentPlayerChangesAfterMoveTaken() {
let firstPlayer = game.currentPlayer
// White player move leftmost pawn
try! game.currentPlayer.movePiece(fromLocation: BoardLocation(x: 0, y: 1),
toLocation: BoardLocation(x: 0, y: 2))
XCTAssert(game.currentPlayer !== firstPlayer)
}
*/
// MARK: Game State Tests
func testGameStateEquatableReturnsCorrectEquability() {
// Match in progress
XCTAssertTrue(Game.State.inProgress == Game.State.inProgress)
// Match stale mate
XCTAssertTrue(Game.State.staleMate(color: .white) == Game.State.staleMate(color: .white))
XCTAssertTrue(Game.State.staleMate(color: .black) == Game.State.staleMate(color: .black))
XCTAssertFalse(Game.State.staleMate(color: .white) == Game.State.staleMate(color: .black))
XCTAssertFalse(Game.State.staleMate(color: .black) == Game.State.staleMate(color: .white))
// Match won
XCTAssertTrue(Game.State.won(color: .white) == Game.State.won(color: .white))
XCTAssertTrue(Game.State.won(color: .black) == Game.State.won(color: .black))
XCTAssertFalse(Game.State.won(color: .white) == Game.State.won(color: .black))
XCTAssertFalse(Game.State.won(color: .black) == Game.State.won(color: .white))
// Mismatches
XCTAssertFalse(Game.State.inProgress == Game.State.won(color: .black))
XCTAssertFalse(Game.State.inProgress == Game.State.staleMate(color: .white))
XCTAssertFalse(Game.State.staleMate(color: .white) == Game.State.won(color: .white))
}
func testGameStartsInProgress() {
XCTAssertTrue(game.state == Game.State.inProgress)
}
func testAfterMovesGameIsStillInProgressState() {
// White player moves pawn
do {
try whitePlayer.movePiece(fromLocation: BoardLocation(x: 0, y: 1),
toLocation: BoardLocation(x: 0, y: 2))
} catch {
XCTFail()
return
}
// Black player moves pawn
do {
try blackPlayer.movePiece(fromLocation: BoardLocation(x: 7, y: 6),
toLocation: BoardLocation(x: 7, y: 5))
} catch {
XCTFail()
return
}
XCTAssertTrue(game.state == Game.State.inProgress)
}
func testStaleMateScenarioResultsInStaleMateState() {
game = Game(firstPlayer: whitePlayer,
secondPlayer: blackPlayer,
board: Board.whiteInStaleMateScenario(),
colorToMove: .black)
game.playerDidMakeMove(player: blackPlayer, boardOperations: [])
XCTAssertTrue(game.state == Game.State.staleMate(color: .white))
}
func testCheckMateScenarioResultsInWonState() {
game = Game(firstPlayer: whitePlayer,
secondPlayer: blackPlayer,
board: Board.whiteInCheckMateScenario(),
colorToMove: .black)
game.playerDidMakeMove(player: blackPlayer, boardOperations: [])
XCTAssertTrue(game.state == Game.State.won(color: .black))
}
}
+53
View File
@@ -0,0 +1,53 @@
//
// OpeningsTests.swift
// Example
//
// Created by Steve Barnegren on 31/01/2017.
// Copyright © 2017 CocoaPods. All rights reserved.
//
import XCTest
@testable import SwiftChess
class OpeningsTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testOpeningsContainValidMoves() {
let openings = Opening.allOpenings()
for opening in openings {
print("Testing \(opening)")
var board = Board(state: .newGame)
for (source, target) in opening.moveLocations(){
// Assert that the piece exists
guard let piece = board.getPiece(at: source) else {
XCTFail("No piece at \(source)")
break
}
// Assert the piece can move
if piece.movement.canPieceMove(fromLocation: source, toLocation: target, board: board) {
board.movePiece(fromLocation: source, toLocation: target)
}
else{
XCTFail("Cannot move piece from \(source) to \(target)")
}
}
}
}
}
+277 -3
View File
@@ -683,7 +683,6 @@ class PieceMovementTests: XCTestCase {
let movement = PieceMovementKnight()
XCTAssertFalse(movement.canPieceMove(fromLocation: BoardLocation(index: pieceIndex), toLocation: BoardLocation(index: kingIndex), board: board.board))
}
// MARK: - King Movement
@@ -811,7 +810,8 @@ class PieceMovementTests: XCTestCase {
let movement = PieceMovementKing()
XCTAssertFalse(movement.canPieceMove(fromLocation: BoardLocation(index: pieceIndex), toLocation: BoardLocation(index: kingIndex), board: board.board))
}
// MARK: - Pawn Movement
@@ -1147,6 +1147,275 @@ class PieceMovementTests: XCTestCase {
XCTAssertFalse(movement.canPieceMove(fromLocation: BoardLocation(index: pieceIndex), toLocation: BoardLocation(index: kingIndex), board: board.board))
}
// MARK: - Test pawn En Passant
func makeGame(board: Board, colorToMove: Color) -> Game {
let whitePlayer = Human(color: .white)
let blackPlayer = Human(color: .black)
let game = Game(firstPlayer: whitePlayer, secondPlayer: blackPlayer, board: board, colorToMove: colorToMove)
return game
}
func testPawnEnPassantFlagIsTrueAfterMoveTwoSpaces() {
let board = Board(state: .newGame)
let startLocation = BoardLocation(x: 0, y: 1)
let targetLocation = BoardLocation(x: 0, y: 3)
let game = makeGame(board: board, colorToMove: .white)
guard let whitePlayer = game.currentPlayer as? Human else {
fatalError()
}
do {
try whitePlayer.movePiece(fromLocation: startLocation, toLocation: targetLocation)
} catch {
fatalError()
}
guard let piece = game.board.getPiece(at: targetLocation) else {
fatalError()
}
XCTAssertTrue(piece.color == .white)
XCTAssertTrue(piece.type == .pawn)
XCTAssertTrue(piece.canBeTakenByEnPassant == true)
}
func testPawnEnPassantFlagIsFalseAfterMoveOneSpace() {
let board = Board(state: .newGame)
let startLocation = BoardLocation(x: 0, y: 1)
let targetLocation = BoardLocation(x: 0, y: 2)
let game = makeGame(board: board, colorToMove: .white)
guard let whitePlayer = game.currentPlayer as? Human else {
fatalError()
}
do {
try whitePlayer.movePiece(fromLocation: startLocation, toLocation: targetLocation)
} catch {
fatalError()
}
guard let piece = game.board.getPiece(at: targetLocation) else {
fatalError()
}
XCTAssertTrue(piece.color == .white)
XCTAssertTrue(piece.type == .pawn)
XCTAssertTrue(piece.canBeTakenByEnPassant == false)
}
func testPawnEnPassantFlagIsResetAfterSubsequentMove() {
// White moves pawn
let board = Board(state: .newGame)
let startLocation = BoardLocation(x: 0, y: 1)
let targetLocation = BoardLocation(x: 0, y: 2)
let game = makeGame(board: board, colorToMove: .white)
guard let whitePlayer = game.currentPlayer as? Human else {
fatalError()
}
do {
try whitePlayer.movePiece(fromLocation: startLocation, toLocation: targetLocation)
} catch {
fatalError()
}
// Black moves pawn
guard let blackPlayer = game.currentPlayer as? Human else {
fatalError()
}
guard blackPlayer.color == .black else {
fatalError()
}
do {
try blackPlayer.movePiece(fromLocation: BoardLocation(x: 0, y: 6), toLocation: BoardLocation(x: 0, y: 5))
} catch {
fatalError()
}
// Check white pawn flag is false
guard let piece = game.board.getPiece(at: targetLocation) else {
fatalError()
}
XCTAssertTrue(piece.color == .white)
XCTAssertTrue(piece.type == .pawn)
XCTAssertTrue(piece.canBeTakenByEnPassant == false)
}
func testWhitePawnCanTakeOpponentUsingEnPassant() {
let board = ASCIIBoard(pieces: "- - - - - - - g" +
"p - - - - - - -" +
"+ - - - - - - -" +
"* P - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - G" )
let game = makeGame(board: board.board, colorToMove: .black)
let blackPlayer = game.blackPlayer as! Human
let whitePlayer = game.whitePlayer as! Human
// Black move two spaces
do {
try blackPlayer.movePiece(fromLocation: board.locationOfCharacter("p"), toLocation: board.locationOfCharacter("*"))
} catch {
fatalError()
}
// White should be able to take the black pawn using the en passant rule
let pieceMovement = PieceMovementPawn()
XCTAssertTrue(pieceMovement.canPieceMove(fromLocation: board.locationOfCharacter("P"), toLocation: board.locationOfCharacter("+"), board: game.board),
"Expected white to be able to make en passant move")
do {
try whitePlayer.movePiece(fromLocation: board.locationOfCharacter("P"), toLocation: board.locationOfCharacter("+"))
} catch {
XCTFail("Expected white to be able to execute en passant move")
}
XCTAssertTrue(game.board.getPiece(at: board.locationOfCharacter("*")) == nil, "Expected black pawn to be removed from board")
}
func testBlackPawnCanTakeOpponentUsingEnPassant() {
let board = ASCIIBoard(pieces: "- - - - - - - g" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"* p - - - - - -" +
"+ - - - - - - -" +
"P - - - - - - -" +
"- - - - - - - G" )
let game = makeGame(board: board.board, colorToMove: .white)
let whitePlayer = game.whitePlayer as! Human
let blackPlayer = game.blackPlayer as! Human
// White move two spaces
do {
try whitePlayer.movePiece(fromLocation: board.locationOfCharacter("P"), toLocation: board.locationOfCharacter("*"))
} catch {
fatalError()
}
// Black should be able to take the white pawn using the en passant rule
let pieceMovement = PieceMovementPawn()
XCTAssertTrue(pieceMovement.canPieceMove(fromLocation: board.locationOfCharacter("p"), toLocation: board.locationOfCharacter("+"), board: game.board),
"Expected black to be able to make en passant move")
do {
try blackPlayer.movePiece(fromLocation: board.locationOfCharacter("p"), toLocation: board.locationOfCharacter("+"))
} catch {
XCTFail("Expected black to be able to execute en passant move")
}
XCTAssertTrue(game.board.getPiece(at: board.locationOfCharacter("*")) == nil, "Expected white pawn to be removed from board")
}
func testWhitePawnCannotTakeOpponentUsingEnPassantIfMoveNotMadeImmediately() {
let board = ASCIIBoard(pieces: "- - - - - - - g" +
"p - - - - - - %" +
"+ - - - - - - -" +
"* P - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - &" +
"- - - - - - - G" )
let game = makeGame(board: board.board, colorToMove: .black)
let whitePlayer = game.whitePlayer as! Human
let blackPlayer = game.blackPlayer as! Human
// Black move two spaces
do {
try blackPlayer.movePiece(fromLocation: board.locationOfCharacter("p"), toLocation: board.locationOfCharacter("*"))
} catch {
fatalError()
}
// White moves king
do {
try whitePlayer.movePiece(fromLocation: board.locationOfCharacter("G"), toLocation: board.locationOfCharacter("&"))
} catch {
fatalError()
}
// Black moves king
do {
try blackPlayer.movePiece(fromLocation: board.locationOfCharacter("g"), toLocation: board.locationOfCharacter("%"))
} catch {
fatalError()
}
// White should not be able to take the black pawn using the en passant rule
let pieceMovement = PieceMovementPawn()
XCTAssertFalse(pieceMovement.canPieceMove(fromLocation: board.locationOfCharacter("P"), toLocation: board.locationOfCharacter("+"), board: game.board))
}
func testBlackPawnCannotTakeOpponentUsingEnPassantIfMoveNotMadeImmediately() {
let board = ASCIIBoard(pieces: "- - - - - - - g" +
"- - - - - - - %" +
"- - - - - - - -" +
"- - - - - - - -" +
"* p - - - - - -" +
"+ - - - - - - -" +
"P - - - - - - &" +
"- - - - - - - G" )
let game = makeGame(board: board.board, colorToMove: .white)
let whitePlayer = game.whitePlayer as! Human
let blackPlayer = game.blackPlayer as! Human
// White moves pawn two spaces
do {
try whitePlayer.movePiece(fromLocation: board.locationOfCharacter("P"), toLocation: board.locationOfCharacter("*"))
} catch {
fatalError()
}
// Black moves king
do {
try blackPlayer.movePiece(fromLocation: board.locationOfCharacter("g"), toLocation: board.locationOfCharacter("%"))
} catch {
fatalError()
}
// White moves king
do {
try whitePlayer.movePiece(fromLocation: board.locationOfCharacter("G"), toLocation: board.locationOfCharacter("&"))
} catch {
fatalError()
}
// Black should not be able to take the white pawn using the en passant rule
let pieceMovement = PieceMovementPawn()
XCTAssertFalse(pieceMovement.canPieceMove(fromLocation: board.locationOfCharacter("p"), toLocation: board.locationOfCharacter("+"), board: game.board))
}
// MARK: - Queen movement
func testQueenCannotMoveToInvalidPositionFromCentre(){
@@ -1274,6 +1543,8 @@ class PieceMovementTests: XCTestCase {
XCTAssertFalse(movement.canPieceMove(fromLocation: BoardLocation(index: pieceIndex), toLocation: BoardLocation(index: kingIndex), board: board.board))
}
// MARK: - Rook Movement
func testRookCannotMoveToInvalidPositionFromCentre(){
@@ -1400,7 +1671,8 @@ class PieceMovementTests: XCTestCase {
let movement = PieceMovementRook()
XCTAssertFalse(movement.canPieceMove(fromLocation: BoardLocation(index: pieceIndex), toLocation: BoardLocation(index: kingIndex), board: board.board))
}
// MARK: - Bishop movement
@@ -1529,6 +1801,8 @@ class PieceMovementTests: XCTestCase {
let movement = PieceMovementBishop()
XCTAssertFalse(movement.canPieceMove(fromLocation: BoardLocation(index: pieceIndex), toLocation: BoardLocation(index: kingIndex), board: board.board))
}
+155 -1
View File
@@ -55,6 +55,160 @@ class PlayerTests: XCTestCase {
XCTAssert(game.whitePlayer.canMovePiece(fromLocation: location, toLocation: location) == false)
}
// TODO: Add tests for move error throw values
// MARK: - Move Errors
func gameForTestingCallbacks(board: Board, color: Color) -> Game {
let whitePlayer = Human(color: .white)
let blackPlayer = Human(color: .black)
let game = Game(firstPlayer: whitePlayer, secondPlayer: blackPlayer, board: board, colorToMove: color)
return game
}
func testMoveInToCheckErrorIsThrownByMovingQueen() {
let board = ASCIIBoard(pieces: "- - - - * - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"G - - - Q - - r" +
"- - - - - - - -" )
let queenLocation = board.locationOfCharacter("Q")
let targetLocation = board.locationOfCharacter("*")
let game = gameForTestingCallbacks(board: board.board, color: .white)
guard let player = game.currentPlayer as? Human else {
fatalError()
}
// Assert that the correct error is thrown
XCTAssertTrue(player.canMovePieceWithError(fromLocation: queenLocation, toLocation: targetLocation).error == .cannotMoveInToCheck)
}
func testMoveInToCheckErrorIsThrownByMovingPawn() {
let board = ASCIIBoard(pieces: "- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - * - - -" +
"G - - - P - - r" +
"- - - - - - - -" )
let pieceLocation = board.locationOfCharacter("P")
let targetLocation = board.locationOfCharacter("*")
let game = gameForTestingCallbacks(board: board.board, color: .white)
guard let player = game.currentPlayer as? Human else {
fatalError()
}
// Assert that the correct error is thrown
XCTAssertTrue(player.canMovePieceWithError(fromLocation: pieceLocation, toLocation: targetLocation).error == .cannotMoveInToCheck)
}
func testMoveInToCheckErrorIsThrownByMovingKnight() {
let board = ASCIIBoard(pieces: "- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - * - - - -" +
"- - - - - - - -" +
"G - - - K - - r" +
"- - - - - - - -" )
let pieceLocation = board.locationOfCharacter("K")
let targetLocation = board.locationOfCharacter("*")
let game = gameForTestingCallbacks(board: board.board, color: .white)
guard let player = game.currentPlayer as? Human else {
fatalError()
}
// Assert that the correct error is thrown
XCTAssertTrue(player.canMovePieceWithError(fromLocation: pieceLocation, toLocation: targetLocation).error == .cannotMoveInToCheck)
}
func testMoveInToCheckErrorIsThrownByMovingBishop() {
let board = ASCIIBoard(pieces: "- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - * - - - - -" +
"- - - - - - - -" +
"G - - - B - - r" +
"- - - - - - - -" )
let pieceLocation = board.locationOfCharacter("B")
let targetLocation = board.locationOfCharacter("*")
let game = gameForTestingCallbacks(board: board.board, color: .white)
guard let player = game.currentPlayer as? Human else {
fatalError()
}
// Assert that the correct error is thrown
XCTAssertTrue(player.canMovePieceWithError(fromLocation: pieceLocation, toLocation: targetLocation).error == .cannotMoveInToCheck)
}
func testMoveInToCheckErrorIsThrownByMovingRook() {
let board = ASCIIBoard(pieces: "- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - * - - -" +
"- - - - - - - -" +
"G - - - R - - r" +
"- - - - - - - -" )
let pieceLocation = board.locationOfCharacter("R")
let targetLocation = board.locationOfCharacter("*")
let game = gameForTestingCallbacks(board: board.board, color: .white)
guard let player = game.currentPlayer as? Human else {
fatalError()
}
// Assert that the correct error is thrown
XCTAssertTrue(player.canMovePieceWithError(fromLocation: pieceLocation, toLocation: targetLocation).error == .cannotMoveInToCheck)
}
func testMoveInToCheckErrorIsThrownByMovingKing() {
let board = ASCIIBoard(pieces: "- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"- - - - - - - -" +
"* - - - - - - r" +
"G - - - - - - -" )
let pieceLocation = board.locationOfCharacter("G")
let targetLocation = board.locationOfCharacter("*")
let game = gameForTestingCallbacks(board: board.board, color: .white)
guard let player = game.currentPlayer as? Human else {
fatalError()
}
// Assert that the correct error is thrown
XCTAssertTrue(player.canMovePieceWithError(fromLocation: pieceLocation, toLocation: targetLocation).error == .cannotMoveInToCheck)
}
}
+14 -16
View File
@@ -1,28 +1,26 @@
# SwiftChess
[![CI Status](http://img.shields.io/travis/Steve Barnegren/SwiftChess.svg?style=flat)](https://travis-ci.org/Steve Barnegren/SwiftChess)
[![Version](https://img.shields.io/cocoapods/v/SwiftChess.svg?style=flat)](http://cocoapods.org/pods/SwiftChess)
[![License](https://img.shields.io/cocoapods/l/SwiftChess.svg?style=flat)](http://cocoapods.org/pods/SwiftChess)
[![Platform](https://img.shields.io/cocoapods/p/SwiftChess.svg?style=flat)](http://cocoapods.org/pods/SwiftChess)
SwiftChess is a chess engine written in Swift.
Rather than a library that you can call to assist in making valid chess moves, SwiftChess aims to be a complete chess game, minus the UI.
SwiftChess also includes a complete AI implementation.
There's no documentation for now, as the public api is still in flux, but there is a complete example project for iOS.
SwiftChess also has a reasonably comprehensive set of unit tests.
## Example
To run the example project, clone the repo, and run `pod install` from the Example directory first.
Run `Example/Example.xcodeproj`
## Requirements
## Installation
SwiftChess is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod "SwiftChess"
```
The example app can run *player vs player*, *player vs AI*, *AI vs AI* matches
## Author
Steve Barnegren, steve.barnegren@gmail.com
Steve Barnegren
[Follow me on Twitter](https://twitter.com/stevebarnegren)
## License
+3 -3
View File
@@ -21,16 +21,16 @@ Pod::Spec.new do |s|
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://github.com/<GITHUB_USERNAME>/SwiftChess'
s.homepage = 'https://github.com/SteveBarnegren/SwiftChess'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'Steve Barnegren' => 'steve.barnegren@gmail.com' }
s.source = { :git => 'https://github.com/<GITHUB_USERNAME>/SwiftChess.git', :tag => s.version.to_s }
s.source = { :git => 'https://github.com/SteveBarnegren/SwiftChess.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '8.0'
s.source_files = 'SwiftChess/Classes/**/*'
s.source_files = 'SwiftChess/Source/**/*'
# s.resource_bundles = {
# 'SwiftChess' => ['SwiftChess/Assets/*.png']
+8 -8
View File
@@ -43,16 +43,16 @@ struct AIConfiguration {
}
mutating func setDefualtValues() {
boardRaterCountPiecesWeighting = 1
boardRaterBoardDominanceWeighting = 1
boardRaterCenterOwnershipWeighting = 1
boardRaterCenterDominanceWeighting = 1
boardRaterThreatenedPiecesWeighting = 1
boardRaterThreatenedPiecesOwnPiecesMultiplier = 2
boardRaterCountPiecesWeighting = 3 //1
boardRaterBoardDominanceWeighting = 0.1
boardRaterCenterOwnershipWeighting = 0.3
boardRaterCenterDominanceWeighting = 0.3
boardRaterCenterFourOccupationWeighting = 0.3
boardRaterThreatenedPiecesWeighting = 1 // 1.5
boardRaterThreatenedPiecesOwnPiecesMultiplier = 20 // Higher values will be more defensive
boardRaterPawnProgressionWeighting = 1
boardRaterKingSurroundingPossessionWeighting = 1
boardRaterKingSurroundingPossessionWeighting = 0.3
boardRaterCheckMateOpportunityWeighting = 2
boardRaterCenterFourOccupationWeighting = 1
}
}
+137 -81
View File
@@ -13,6 +13,7 @@ open class AIPlayer : Player {
let boardRaters : [BoardRater]!
let configuration = AIConfiguration() // <-- We should pass this in eventually
var openingMoves = [OpeningMove]()
public init(color: Color){
@@ -24,105 +25,44 @@ open class AIPlayer : Player {
BoardRaterThreatenedPieces(configuration: configuration),
BoardRaterPawnProgression(configuration: configuration),
BoardRaterKingSurroundingPossession(configuration: configuration),
BoardRaterCheckMateOpportunity(configuration: configuration)
BoardRaterCheckMateOpportunity(configuration: configuration),
BoardRaterCenterFourOccupation(configuration: configuration)
]
openingMoves = Opening.allOpeningMoves(forColor: color)
super.init()
self.color = color
}
public func makeMove() {
print("\n\n****** Make Move ******");
let board = game.board
// Build list of possible moves with ratings
var move: Move!
var possibleMoves = [Move]()
for sourceLocation in BoardLocation.all {
guard let piece = board.getPiece(at: sourceLocation) else {
continue
}
if piece.color != color {
continue
}
for targetLocation in BoardLocation.all {
guard canMovePiece(fromLocation: sourceLocation, toLocation: targetLocation) else {
continue
}
// Make move
var resultBoard = board
resultBoard.movePiece(fromLocation: sourceLocation, toLocation: targetLocation)
// Promote pawns
let pawnsToPromoteLocations = resultBoard.getLocationsOfPromotablePawns(color: color)
assert(pawnsToPromoteLocations.count < 2, "There should only ever be one pawn to promote at any time")
if pawnsToPromoteLocations.count > 0 {
resultBoard = promotePawnsOnBoard(resultBoard)
}
// Rate
let rating = ratingForBoard(resultBoard)
let move = Move(type: .singlePiece(sourceLocation: sourceLocation, targetLocation: targetLocation),
rating: rating)
possibleMoves.append(move)
}
// Get an opening move
if let openingMove = openingMoveForBoard(board){
print("Playing opening move")
move = openingMove
}
// Add castling moves
let castleSides: [CastleSide] = [.kingSide, .queenSide]
for side in castleSides {
guard game.board.canColorCastle(color: color, side: side) else {
continue
}
// Perform the castling move
var resultBoard = board
resultBoard.performCastle(color: color, side: side)
// Rate
let rating = ratingForBoard(resultBoard)
let move = Move(type: .castle(color: color, side: side), rating: rating)
possibleMoves.append(move)
// Or, get the Highest rated move
else{
move = highestRatedMoveOnBoard(board)
}
print("Found \(possibleMoves.count) possible moves")
// If there are no possible moves, we must be in stale mate
if possibleMoves.count == 0 {
print("There are no possible moves!!!!");
}
// Choose move with highest rating
var highestRating = possibleMoves.first!.rating
var highestRatedMove = possibleMoves.first!
for move in possibleMoves {
if move.rating > highestRating {
highestRating = move.rating
highestRatedMove = move;
}
print("rating: \(move.rating)")
}
print("HIGHEST MOVE RATING: \(highestRating)")
// Make move
var operations = [BoardOperation]()
switch highestRatedMove.type {
switch move.type {
case .singlePiece(let sourceLocation, let targetLocation):
operations = game.board.movePiece(fromLocation: sourceLocation, toLocation: targetLocation)
print("Chose move (\(sourceLocation.x),\(sourceLocation.y)) -> (\(targetLocation.x),\(targetLocation.y))");
case .castle(let color, let side):
operations = game.board.performCastle(color: color, side: side)
print("Chose Castling move");
}
// Promote pawns
@@ -139,12 +79,130 @@ open class AIPlayer : Player {
self.game.playerDidMakeMove(player: self, boardOperations: operations)
}
func openingMoveForBoard(_ board: Board) -> Move? {
let possibleMoves = openingMoves.filter{
$0.board == board
}
guard possibleMoves.count > 0 else{
return nil
}
let openingMove = possibleMoves[Int(arc4random()) % possibleMoves.count]
return Move(type: .singlePiece(sourceLocation: openingMove.fromLocation,
targetLocation: openingMove.toLocation),
rating: 0)
}
func highestRatedMoveOnBoard(_ board: Board) -> Move {
var possibleMoves = [Move]()
for sourceLocation in BoardLocation.all {
guard let piece = board.getPiece(at: sourceLocation) else {
continue
}
if piece.color != color {
continue
}
for targetLocation in BoardLocation.all {
guard canAIMovePiece(fromLocation: sourceLocation, toLocation: targetLocation) else {
continue
}
// Make move
var resultBoard = board
resultBoard.movePiece(fromLocation: sourceLocation, toLocation: targetLocation)
// Promote pawns
let pawnsToPromoteLocations = resultBoard.getLocationsOfPromotablePawns(color: color)
assert(pawnsToPromoteLocations.count < 2, "There should only ever be one pawn to promote at any time")
if pawnsToPromoteLocations.count > 0 {
resultBoard = promotePawnsOnBoard(resultBoard)
}
// Rate
print("(\(sourceLocation.x),\(sourceLocation.y)) -> (\(targetLocation.x),\(targetLocation.y))")
let rating = ratingForBoard(resultBoard)
let move = Move(type: .singlePiece(sourceLocation: sourceLocation, targetLocation: targetLocation),
rating: rating)
possibleMoves.append(move)
print("Rating: \(rating)")
}
}
// Add castling moves
let castleSides: [CastleSide] = [.kingSide, .queenSide]
for side in castleSides {
guard game.board.canColorCastle(color: color, side: side) else {
continue
}
// Perform the castling move
var resultBoard = board
resultBoard.performCastle(color: color, side: side)
// Rate
let rating = ratingForBoard(resultBoard)
let move = Move(type: .castle(color: color, side: side), rating: rating)
possibleMoves.append(move)
}
print("Found \(possibleMoves.count) possible moves")
// If there are no possible moves, we must be in stale mate
if possibleMoves.count == 0 {
print("There are no possible moves!!!!");
}
// Choose move with highest rating
var highestRating = possibleMoves.first!.rating
var highestRatedMove = possibleMoves.first!
for move in possibleMoves {
if move.rating > highestRating {
highestRating = move.rating
highestRatedMove = move;
}
//print("rating: \(move.rating)")
}
return highestRatedMove;
}
func canAIMovePiece(fromLocation: BoardLocation, toLocation: BoardLocation) -> Bool {
// This is a stricter version of the canMove function, used by the AI, that returns false for errors
let canMove = canMovePieceWithError(fromLocation: fromLocation, toLocation: toLocation)
if canMove.error != nil {
return false
}
return canMove.result
}
func ratingForBoard(_ board: Board) -> Double {
var rating: Double = 0;
for boardRater in boardRaters {
rating += boardRater.ratingfor(board: board, color: color)
let result = boardRater.ratingfor(board: board, color: color)
let className = "\(boardRater)"
print("\t\(className): \(result)")
rating += result
}
// If opponent is in check mate, set the maximum rating
@@ -200,8 +258,6 @@ open class AIPlayer : Player {
return promotedBoard
}
}
struct Move {
@@ -7,7 +7,6 @@
//
import Foundation
import XCTest
import SwiftChess
func transformASCIIBoardInput(_ input: String) -> String{
@@ -33,7 +32,104 @@ public struct ASCIIBoard {
let artString: String
var stringContainsColors: Bool!
init(pieces artString: String) {
public init(
// Row 8
_ a8: Character,
_ b8: Character,
_ c8: Character,
_ d8: Character,
_ e8: Character,
_ f8: Character,
_ g8: Character,
_ h8: Character,
// Row 7
_ a7: Character,
_ b7: Character,
_ c7: Character,
_ d7: Character,
_ e7: Character,
_ f7: Character,
_ g7: Character,
_ h7: Character,
// Row 6
_ a6: Character,
_ b6: Character,
_ c6: Character,
_ d6: Character,
_ e6: Character,
_ f6: Character,
_ g6: Character,
_ h6: Character,
// Row 5
_ a5: Character,
_ b5: Character,
_ c5: Character,
_ d5: Character,
_ e5: Character,
_ f5: Character,
_ g5: Character,
_ h5: Character,
// Row 4
_ a4: Character,
_ b4: Character,
_ c4: Character,
_ d4: Character,
_ e4: Character,
_ f4: Character,
_ g4: Character,
_ h4: Character,
// Row 3
_ a3: Character,
_ b3: Character,
_ c3: Character,
_ d3: Character,
_ e3: Character,
_ f3: Character,
_ g3: Character,
_ h3: Character,
// Row 2
_ a2: Character,
_ b2: Character,
_ c2: Character,
_ d2: Character,
_ e2: Character,
_ f2: Character,
_ g2: Character,
_ h2: Character,
// Row 1
_ a1: Character,
_ b1: Character,
_ c1: Character,
_ d1: Character,
_ e1: Character,
_ f1: Character,
_ g1: Character,
_ h1: Character
){
var inputAsString =
"\(a8) \(b8) \(c8) \(d8) \(e8) \(f8) \(g8) \(h8)" +
"\(a7) \(b7) \(c7) \(d7) \(e7) \(f7) \(g7) \(h7)" +
"\(a6) \(b6) \(c6) \(d6) \(e6) \(f6) \(g6) \(h6)" +
"\(a5) \(b5) \(c5) \(d5) \(e5) \(f5) \(g5) \(h5)" +
"\(a4) \(b4) \(c4) \(d4) \(e4) \(f4) \(g4) \(h4)" +
"\(a3) \(b3) \(c3) \(d3) \(e3) \(f3) \(g3) \(h3)" +
"\(a2) \(b2) \(c2) \(d2) \(e2) \(f2) \(g2) \(h2)" +
"\(a1) \(b1) \(c1) \(d1) \(e1) \(f1) \(g1) \(h1)";
// Transform
inputAsString = transformASCIIBoardInput(inputAsString)
// Check string format
assert(inputAsString.characters.count == 64, "ASCII board art must be 128 characters long")
self.artString = inputAsString
self.stringContainsColors = false
}
public init(pieces artString: String) {
var artString = artString
@@ -41,20 +137,20 @@ public struct ASCIIBoard {
artString = transformASCIIBoardInput(artString)
// Check string format
XCTAssertEqual(artString.characters.count, 64, "ASCII board art must be 128 characters long")
assert(artString.characters.count == 64, "ASCII board art must be 128 characters long")
self.artString = artString
self.stringContainsColors = false
}
init(colors artString: String) {
public init(colors artString: String) {
self.init(pieces: artString)
self.stringContainsColors = true
}
var board: Board {
public var board: Board {
var boardArt = artString
@@ -67,15 +163,14 @@ public struct ASCIIBoard {
var board = Board(state: .empty)
// Clear all pieces on the board
for i in 0..<64 {
board.squares[i].piece = nil;
(0..<64).forEach{
board.squares[$0].piece = nil;
}
// Setup pieces from ascii art
for i in 0..<64 {
let character = boardArt[boardArt.characters.index(boardArt.startIndex, offsetBy: i)]
board.squares[i].piece = pieceFromCharacter(character)
(0..<64).forEach{
let character = boardArt[boardArt.characters.index(boardArt.startIndex, offsetBy: $0)]
board.squares[$0].piece = pieceFromCharacter(character)
}
return board
@@ -126,7 +221,7 @@ public struct ASCIIBoard {
index = artString.characters.distance(from: artString.startIndex, to: idx)
}
XCTAssert(index != nil, "Unable to find index of character: \(character)")
assert(index != nil, "Unable to find index of character: \(character)")
return index!
}
@@ -140,11 +235,10 @@ public struct ASCIIBoard {
var indexes = [Int]()
for i in 0..<64 {
let aCharacter = artString[artString.characters.index(artString.startIndex, offsetBy: i)]
(0..<64).forEach{
let aCharacter = artString[artString.characters.index(artString.startIndex, offsetBy: $0)]
if character == aCharacter {
indexes.append(i)
indexes.append($0)
}
}
@@ -157,9 +251,8 @@ public struct ASCIIBoard {
var locations = [BoardLocation]()
for index in indexes {
let location = BoardLocation(index: index)
indexes.forEach{
let location = BoardLocation(index: $0)
locations.append(location)
}
+106 -120
View File
@@ -13,117 +13,26 @@ public enum CastleSide {
case queenSide
}
// MARK: - ****** BoardStride ******
public struct BoardStride {
public var x: Int;
public var y: Int;
public init(x: Int, y: Int){
self.x = x;
self.y = y;
}
}
// MARK: - ****** BoardLocation ******
public struct BoardLocation : Equatable {
public var index: Int
public static var all: [BoardLocation] {
// TODO: using a computed property could be expensive, can we store this so it doesn't need to be computed each time?
var locations = [BoardLocation]()
for i in 0..<64 {
locations.append(BoardLocation(index: i))
}
return locations
}
public var x: Int {
return index % 8
}
public var y: Int {
return index / 8
}
public init(index: Int) {
self.index = index
}
public init(x: Int, y: Int) {
self.index = x + (y*8)
}
func isInBounds() -> Bool {
return (index < 64 && index >= 0)
}
func incremented(by offset: Int) -> BoardLocation {
return BoardLocation(index: index + offset)
}
func incrementedBy(x: Int, y: Int) -> BoardLocation {
return self + BoardLocation(x: x, y: y)
}
func incrementedBy(stride: BoardStride) -> BoardLocation {
// TODO: for performance, we should probably only check this if we're in debug mode
if !canIncrementBy(stride: stride) {
print("WARNING! BoardLocation is being incremented by a stride that will result in wrapping! call canIncrementBy(stride: BoardStride) first")
}
return BoardLocation(x: x + stride.x,
y: y + stride.y)
}
func canIncrementBy(stride: BoardStride) -> Bool {
// Check will not wrap to right
if x + stride.x > 7 {
return false
}
// Check will not wrap to left
if x + stride.x < 0 {
return false
}
// Check will not wrap top
if y + stride.y > 7 {
return false
}
// Check will not wrap bottom
if y + stride.y < 0 {
return false
}
return true
}
}
public func ==(lhs: BoardLocation, rhs: BoardLocation) -> Bool {
return lhs.index == rhs.index
}
public func +(left: BoardLocation, right: BoardLocation) -> BoardLocation {
return BoardLocation(index: left.index + right.index)
}
// MARK: - ****** Square ******
public struct Square {
public struct Square : Equatable {
public var piece: Piece?
}
public func ==(lhs: Square, rhs: Square) -> Bool {
switch (lhs.piece, rhs.piece) {
case (.none, .none):
return true
case (.some, .none):
return true
case (.none, .some):
return true
case (.some(let rp), .some(let lp)):
return rp == lp
}
}
// MARK: - ****** Board ******
@@ -141,7 +50,7 @@ public struct Board {
public init(state: InitialState) {
// Setup squares
for i in 0..<64 {
for _ in 0..<64 {
squares.append(Square())
}
@@ -204,23 +113,62 @@ public struct Board {
var operations = [BoardOperation]()
if let piece = getPiece(at: fromLocation) {
let operation = BoardOperation(type: .movePiece, piece: piece, location: toLocation)
operations.append(operation)
guard var movingPiece = getPiece(at: fromLocation) else {
fatalError("There is no piece on at (\(fromLocation.x),\(fromLocation.y))")
}
let operation = BoardOperation(type: .movePiece, piece: movingPiece, location: toLocation)
operations.append(operation)
if let piece = getPiece(at: toLocation) {
let operation = BoardOperation(type: .removePiece, piece: piece, location: toLocation)
if let targetPiece = getPiece(at: toLocation) {
let operation = BoardOperation(type: .removePiece, piece: targetPiece, location: toLocation)
operations.append(operation)
}
squares[toLocation.index].piece = self.squares[fromLocation.index].piece
squares[toLocation.index].piece?.hasMoved = true
squares[fromLocation.index].piece = nil
// If the moving piece is a pawn, check whether it just made an en passent move, and remove the passed piece
IF_EN_PASSANT: if movingPiece.type == .pawn {
let stride = fromLocation.strideTo(location: toLocation)
let enPassentStride = BoardStride(x: stride.x, y: 0)
let enPassentLocation = fromLocation.incrementedBy(stride: enPassentStride)
guard let enPassentPiece = getPiece(at: enPassentLocation) else {
break IF_EN_PASSANT
}
if enPassentPiece.canBeTakenByEnPassant && enPassentPiece.color == movingPiece.color.opposite() {
squares[enPassentLocation.index].piece = nil;
}
}
// Reset en passant flags
resetEnPassantFlags()
// If pawn has moved two squares, then need to update the en passant flag
if movingPiece.type == .pawn {
let startingRow = (movingPiece.color == .white ? 1 : 6)
let twoAheadRow = (movingPiece.color == .white ? 3 : 4)
if fromLocation.y == startingRow && toLocation.y == twoAheadRow {
squares[toLocation.index].piece?.canBeTakenByEnPassant = true
}
}
return operations
}
mutating func resetEnPassantFlags() {
for i in 0..<squares.count {
squares[i].piece?.canBeTakenByEnPassant = false
}
}
// MARK: - Get Specific pieces
func getKing(color: Color) -> Piece {
@@ -322,20 +270,39 @@ public struct Board {
public func isColorInCheck(color: Color) -> Bool {
let kingLocation = getKingLocation(color: color)
// Get the King location
var kingLocation: BoardLocation?
for location in BoardLocation.all {
guard let piece = getPiece(at: location) else {
continue
}
if piece.color == color && piece.type == .king {
kingLocation = location
break
}
}
// If there is no king, then return false (some tests will be run without a king)
if kingLocation == nil {
return false
}
// Work out if we're in check
let oppositionLocations = getLocationsOfColor( color.opposite() )
// Pieces will not move to take the king, so remove it
// Pieces will not move to take the king, so change it for a pawn of the same color
var noKingBoard = self
noKingBoard.squares[kingLocation.index].piece = nil
noKingBoard.squares[kingLocation!.index].piece = Piece(type: .pawn, color: color)
for location in oppositionLocations {
guard let piece = getPiece(at: location) else {
continue
}
if piece.movement.canPieceMove(fromLocation: location, toLocation: kingLocation, board: noKingBoard) {
if piece.movement.canPieceMove(fromLocation: location, toLocation: kingLocation!, board: noKingBoard) {
return true
}
}
@@ -444,6 +411,23 @@ public struct Board {
return (piece.color == color ? true : false)
}
public func possibleMoveLocationsForPiece(atLocation location: BoardLocation) -> [BoardLocation] {
guard let piece = squares[location.index].piece else{
return []
}
var locations = [BoardLocation]()
BoardLocation.all.forEach{
if piece.movement.canPieceMove(fromLocation: location, toLocation: $0, board: self){
locations.append($0)
}
}
return locations
}
// MARK: - Castling
struct CastleMove{
@@ -663,6 +647,8 @@ public struct Board {
print(printString)
}
}
public func ==(lhs: Board, rhs: Board) -> Bool {
return lhs.squares == rhs.squares
}
+129
View File
@@ -0,0 +1,129 @@
//
// BoardLocation.swift
// SwiftChess
//
// Created by Steve Barnegren on 31/01/2017.
// Copyright © 2017 Steve Barnegren. All rights reserved.
//
import Foundation
public struct BoardLocation : Equatable {
public enum GridPosition : Int{
case a1; case b1; case c1; case d1; case e1; case f1; case g1; case h1;
case a2; case b2; case c2; case d2; case e2; case f2; case g2; case h2;
case a3; case b3; case c3; case d3; case e3; case f3; case g3; case h3;
case a4; case b4; case c4; case d4; case e4; case f4; case g4; case h4;
case a5; case b5; case c5; case d5; case e5; case f5; case g5; case h5;
case a6; case b6; case c6; case d6; case e6; case f6; case g6; case h6;
case a7; case b7; case c7; case d7; case e7; case f7; case g7; case h7;
case a8; case b8; case c8; case d8; case e8; case f8; case g8; case h8;
}
public var index: Int
public static var all: [BoardLocation] {
// TODO: using a computed property could be expensive, can we store this so it doesn't need to be computed each time?
var locations = [BoardLocation]()
(0..<64).forEach{
locations.append(BoardLocation(index: $0))
}
return locations
}
public var x: Int {
return index % 8
}
public var y: Int {
return index / 8
}
public var gridPosition: GridPosition {
return GridPosition(rawValue: index)!
}
public init(index: Int) {
self.index = index
}
public init(x: Int, y: Int) {
self.index = x + (y*8)
}
public init(gridPosition: GridPosition){
self.index = gridPosition.rawValue
}
func isInBounds() -> Bool {
return (index < 64 && index >= 0)
}
func incremented(by offset: Int) -> BoardLocation {
return BoardLocation(index: index + offset)
}
func incrementedBy(x: Int, y: Int) -> BoardLocation {
return self + BoardLocation(x: x, y: y)
}
func incrementedBy(stride: BoardStride) -> BoardLocation {
// TODO: for performance, we should probably only check this if we're in debug mode
if !canIncrementBy(stride: stride) {
print("WARNING! BoardLocation is being incremented by a stride that will result in wrapping! call canIncrementBy(stride: BoardStride) first")
}
return BoardLocation(x: x + stride.x,
y: y + stride.y)
}
func canIncrementBy(stride: BoardStride) -> Bool {
// Check will not wrap to right
if x + stride.x > 7 {
return false
}
// Check will not wrap to left
if x + stride.x < 0 {
return false
}
// Check will not wrap top
if y + stride.y > 7 {
return false
}
// Check will not wrap bottom
if y + stride.y < 0 {
return false
}
return true
}
func strideTo(location: BoardLocation) -> BoardStride {
return BoardStride(x: location.x - x,
y: location.y - y)
}
func strideFrom(location: BoardLocation) -> BoardStride {
return BoardStride(x: x - location.x,
y: y - location.y)
}
}
public func ==(lhs: BoardLocation, rhs: BoardLocation) -> Bool {
return lhs.index == rhs.index
}
public func +(left: BoardLocation, right: BoardLocation) -> BoardLocation {
return BoardLocation(index: left.index + right.index)
}
+21
View File
@@ -0,0 +1,21 @@
//
// BoardStride.swift
// SwiftChess
//
// Created by Steve Barnegren on 31/01/2017.
// Copyright © 2017 Steve Barnegren. All rights reserved.
//
import Foundation
public struct BoardStride {
public var x: Int;
public var y: Int;
public init(x: Int, y: Int){
self.x = x;
self.y = y;
}
}
+34 -5
View File
@@ -10,15 +10,38 @@ import Foundation
open class Game {
// MARK: Types
public enum State: Equatable {
case inProgress
case staleMate(color: Color)
case won(color: Color)
public static func ==(lhs: State, rhs: State) -> Bool {
switch (lhs, rhs) {
case (.inProgress, .inProgress):
return true
case (let .staleMate(color1), let staleMate(color2)):
return color1 == color2
case (let .won(color1), let won(color2)):
return color1 == color2
default:
return false
}
}
}
// MARK: Properties
open var board = Board(state: .newGame)
open var whitePlayer: Player!
open var blackPlayer: Player!
open var currentPlayer: Player!
open var state = Game.State.inProgress
open weak var delegate: GameDelegate?
public init(firstPlayer: Player, secondPlayer: Player){
// MARK: Init
public init(firstPlayer: Player, secondPlayer: Player, board: Board = Board(state: .newGame), colorToMove: Color = .white){
self.board = board
// Assign to correct colors
if firstPlayer.color == secondPlayer.color {
@@ -33,10 +56,13 @@ open class Game {
self.blackPlayer.delegate = self
self.whitePlayer.game = self
self.blackPlayer.game = self
self.currentPlayer = self.whitePlayer
self.currentPlayer = (colorToMove == .white ? self.whitePlayer : self.blackPlayer)
}
}
// MARK: - Game : PlayerDelegate
extension Game : PlayerDelegate {
func playerDidMakeMove(player: Player, boardOperations: [BoardOperation]) {
@@ -51,12 +77,14 @@ extension Game : PlayerDelegate {
// Check for game ended
if board.isColorInCheckMate(color: currentPlayer.color.opposite()) {
state = .won(color: currentPlayer.color)
delegate?.gameWonByPlayer(game: self, player: currentPlayer)
return
}
// Check for stalemate
if board.isColorInStalemate(color: currentPlayer.color.opposite()) {
state = .staleMate(color: currentPlayer.color.opposite())
delegate?.gameEndedInStaleMate(game: self)
return
}
@@ -91,6 +119,7 @@ extension Game : PlayerDelegate {
}
// MARK: - GameDelegate
public protocol GameDelegate: class {
// State changes
+167
View File
@@ -0,0 +1,167 @@
//
// Opening.swift
// SwiftChess
//
// Created by Steve Barnegren on 31/01/2017.
// Copyright © 2017 Steve Barnegren. All rights reserved.
//
import Foundation
struct OpeningMove {
let board: Board
let fromLocation: BoardLocation
let toLocation: BoardLocation
}
class Opening {
static func allOpenings() -> [Opening] {
return [
RuyLopez(),
ItalianGame(),
SicilianDefense(),
QueensGambit(),
KingsGambit(),
]
}
static func allOpeningMoves(forColor color: Color) -> [OpeningMove] {
var openingMoves = [OpeningMove]()
allOpenings().forEach{
openingMoves += $0.moves(forColor: color)
}
return openingMoves
}
public func moves(forColor color: Color) -> [OpeningMove] {
var moves = [OpeningMove]()
var board = Board(state: .newGame)
for locations in moveLocations(){
let move = OpeningMove(board: board,
fromLocation: locations.fromLocation,
toLocation: locations.toLocation)
moves.append(move)
board.movePiece(fromLocation: locations.fromLocation,
toLocation: locations.toLocation)
}
// Filter for color
return moves.enumerated().flatMap{ (index, value) in
index % 2 == (color == .white ? 0 : 1) ? value : nil
}
}
/*
public func moveLocations(forColor color: Color) -> [(fromLocation: BoardLocation, toLocation: BoardLocation)] {
return moveLocations().enumerated().flatMap{ (index, value) in
index % 2 == (color == .white ? 0 : 1) ? value : nil
}
}
*/
func moveLocations() -> [(fromLocation: BoardLocation, toLocation: BoardLocation)] {
return moveGridPositions().map{
(BoardLocation(gridPosition: $0), BoardLocation(gridPosition: $1))
}
}
func moveGridPositions() -> [(fromPosition: BoardLocation.GridPosition, toPosition: BoardLocation.GridPosition)] {
fatalError("Must override")
}
}
// MARK: - Ruy Lopez
class RuyLopez: Opening {
override func moveGridPositions() -> [(fromPosition: BoardLocation.GridPosition, toPosition: BoardLocation.GridPosition)] {
let moves: [(BoardLocation.GridPosition, BoardLocation.GridPosition)] = [
(.e2, .e4), // white moves pawn to e4
(.e7, .e5), // black moves pawn to e5
(.g1, .f3), // white moves knight to f3
(.b8, .c6), // black moves knight to c6
(.f1, .b5), // white moves bishop to b5
]
return moves
}
}
// MARK: - Italian Game
class ItalianGame: Opening {
override func moveGridPositions() -> [(fromPosition: BoardLocation.GridPosition, toPosition: BoardLocation.GridPosition)] {
let moves: [(BoardLocation.GridPosition, BoardLocation.GridPosition)] = [
(.e2, .e4), // white moves pawn to e4
(.e7, .e5), // black moves pawn to e5
(.g1, .f3), // white moves knight to f3
(.b8, .c6), // black moves knight to c6
(.f1, .c4), // white moves bishop to c4
]
return moves
}
}
// MARK: - Sicilian Defense
class SicilianDefense: Opening {
override func moveGridPositions() -> [(fromPosition: BoardLocation.GridPosition, toPosition: BoardLocation.GridPosition)] {
let moves: [(BoardLocation.GridPosition, BoardLocation.GridPosition)] = [
(.e2, .e4), // white moves pawn to e4
(.c7, .c5), // black moves pawn to c5
]
return moves
}
}
// MARK: - Queens Gambit
class QueensGambit: Opening {
override func moveGridPositions() -> [(fromPosition: BoardLocation.GridPosition, toPosition: BoardLocation.GridPosition)] {
let moves: [(BoardLocation.GridPosition, BoardLocation.GridPosition)] = [
(.d2, .d4), // white moves pawn to d4
(.d7, .d5), // black moves pawn to d5
(.c2, .c4), // white moves pawn to c4
]
return moves
}
}
// MARK: - King's Gambit
class KingsGambit: Opening {
override func moveGridPositions() -> [(fromPosition: BoardLocation.GridPosition, toPosition: BoardLocation.GridPosition)] {
let moves: [(BoardLocation.GridPosition, BoardLocation.GridPosition)] = [
(.e2, .e4), // white moves pawn to e4
(.e7, .e5), // black moves pawn to e5
(.f2, .f4), // white moves pawn to f4
]
return moves
}
}
+2 -1
View File
@@ -22,7 +22,7 @@ public enum Color {
}
public struct Piece {
public struct Piece : Equatable {
static private var lastAssignedTag = 0
@@ -43,6 +43,7 @@ public struct Piece {
public let color: Color
public var tag: Int!
public var hasMoved = false
public var canBeTakenByEnPassant = false
var movement : PieceMovement {
return PieceMovement.pieceMovementForPieceType(pieceType: self.type)
+19
View File
@@ -355,11 +355,30 @@ open class PieceMovementPawn: PieceMovement {
continue
}
// If the target square has an opponent piece
if let piece = board.getPiece(at: location) {
if piece.color == color.opposite() {
return true
}
}
// If can make en passent move
let enPassentStride = BoardStride(x: stride.x, y: 0)
guard fromLocation.canIncrementBy(stride: enPassentStride) else {
break
}
let enPassentLocation = fromLocation.incrementedBy(stride: enPassentStride)
guard let passingPiece = board.getPiece(at: enPassentLocation) else {
break
}
if passingPiece.canBeTakenByEnPassant && passingPiece.color == color.opposite() {
return true
}
}
return false
@@ -27,6 +27,10 @@
67F779241E1C326D00885B89 /* BoardRaterCenterFourOccupation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F779231E1C326D00885B89 /* BoardRaterCenterFourOccupation.swift */; };
67F9DB6C1E1AC59600C7EC5A /* BoardRaterCheckMateOpportunity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F9DB6B1E1AC59600C7EC5A /* BoardRaterCheckMateOpportunity.swift */; };
67F9DB771E1AD46000C7EC5A /* AIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F9DB761E1AD46000C7EC5A /* AIConfiguration.swift */; };
67F9DB7A1E2438EC00C7EC5A /* ASCIIBoard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F9DB791E2438EC00C7EC5A /* ASCIIBoard.swift */; };
67FD86871E41021E0023335C /* Opening.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67FD86861E41021E0023335C /* Opening.swift */; };
67FD86891E4105B80023335C /* BoardStride.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67FD86881E4105B80023335C /* BoardStride.swift */; };
67FD868B1E4105D40023335C /* BoardLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67FD868A1E4105D40023335C /* BoardLocation.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -63,6 +67,10 @@
67F779231E1C326D00885B89 /* BoardRaterCenterFourOccupation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardRaterCenterFourOccupation.swift; sourceTree = "<group>"; };
67F9DB6B1E1AC59600C7EC5A /* BoardRaterCheckMateOpportunity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardRaterCheckMateOpportunity.swift; sourceTree = "<group>"; };
67F9DB761E1AD46000C7EC5A /* AIConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AIConfiguration.swift; sourceTree = "<group>"; };
67F9DB791E2438EC00C7EC5A /* ASCIIBoard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ASCIIBoard.swift; sourceTree = "<group>"; };
67FD86861E41021E0023335C /* Opening.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Opening.swift; sourceTree = "<group>"; };
67FD86881E4105B80023335C /* BoardStride.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardStride.swift; sourceTree = "<group>"; };
67FD868A1E4105D40023335C /* BoardLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoardLocation.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -123,6 +131,9 @@
67A9C9F81DE64D2500510FB8 /* Source */ = {
isa = PBXGroup;
children = (
67F9DB791E2438EC00C7EC5A /* ASCIIBoard.swift */,
67FD86881E4105B80023335C /* BoardStride.swift */,
67FD868A1E4105D40023335C /* BoardLocation.swift */,
67A9C9FB1DE64D2500510FB8 /* Board.swift */,
67A9C9FD1DE64D2500510FB8 /* Piece.swift */,
67A9C9FE1DE64D2500510FB8 /* PieceMovement.swift */,
@@ -148,6 +159,7 @@
children = (
67A9C9FA1DE64D2500510FB8 /* AIPlayer.swift */,
67F9DB761E1AD46000C7EC5A /* AIConfiguration.swift */,
67FD86851E4101DD0023335C /* Openings */,
67D54A4B1DE6D8D200C12258 /* BoardRaters */,
);
name = AIPlayer;
@@ -169,6 +181,14 @@
name = BoardRaters;
sourceTree = "<group>";
};
67FD86851E4101DD0023335C /* Openings */ = {
isa = PBXGroup;
children = (
67FD86861E41021E0023335C /* Opening.swift */,
);
name = Openings;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -286,11 +306,15 @@
67A9CA051DE64D2500510FB8 /* PieceMovement.swift in Sources */,
6719898B1DFFE0D80053EA3D /* BoardRaterBoardDominance.swift in Sources */,
67D54A481DE6D34100C12258 /* Player.swift in Sources */,
67F9DB7A1E2438EC00C7EC5A /* ASCIIBoard.swift in Sources */,
67F9DB6C1E1AC59600C7EC5A /* BoardRaterCheckMateOpportunity.swift in Sources */,
67FD86891E4105B80023335C /* BoardStride.swift in Sources */,
671989831DEB11900053EA3D /* BoardRaterCenterOwnership.swift in Sources */,
67A9CA061DE64D2500510FB8 /* Human.swift in Sources */,
676EF7C31E15A8A500E275B4 /* BoardRaterKingSurroundingPossession.swift in Sources */,
67FD86871E41021E0023335C /* Opening.swift in Sources */,
67F9DB771E1AD46000C7EC5A /* AIConfiguration.swift in Sources */,
67FD868B1E4105D40023335C /* BoardLocation.swift in Sources */,
67D54A4D1DE6DE1400C12258 /* BoardRaterCountPieces.swift in Sources */,
09A4C0271E013950000CFBF4 /* BoardRaterThreatenedPieces.swift in Sources */,
67D54A6B1DEAF16200C12258 /* BoardOperation.swift in Sources */,