From e286e7e78a8dd69a3aa3c87e1792cc8bd8f18948 Mon Sep 17 00:00:00 2001 From: Mathieu GROUSSEAU Date: Fri, 24 Jan 2025 22:07:43 +0100 Subject: [PATCH] Finished Rules..? --- CLI/CLI/main.swift | 83 +++++++---- Model/Sources/Model/Direction.swift | 2 +- Model/Sources/Model/Player.swift | 2 +- ...tFourRules.swift => FourInARowRules.swift} | 28 ++-- Model/Sources/Model/Rules/Move.swift | 13 +- Model/Sources/Model/Rules/Rules.swift | 2 +- Model/Tests/ModelTests/EmptyBoardTests.swift | 2 +- .../Rules/FourInARowRulesTests.swift | 140 ++++++++++++++++++ 8 files changed, 226 insertions(+), 46 deletions(-) rename Model/Sources/Model/Rules/{ConnectFourRules.swift => FourInARowRules.swift} (79%) create mode 100644 Model/Tests/ModelTests/Rules/FourInARowRulesTests.swift diff --git a/CLI/CLI/main.swift b/CLI/CLI/main.swift index e7d1229..494613f 100644 --- a/CLI/CLI/main.swift +++ b/CLI/CLI/main.swift @@ -3,43 +3,68 @@ import Foundation import Model import CustomTypes -guard var board = Board(columns: 5, rows: 5) else { - print("Failed to create board.") - exit(EXIT_FAILURE) +var rules = FourInARowRules() + +var board = rules.createBoard() + +guard rules.isValid(board: board) else { + fatalError("Board is invalid") } -print(board.debugDescription) -print(board) +print(rules.state) -for i in 0...2 { - for _ in 0...i { - let dropAt = board.getInsertionCoordinates(from: .Top, offset: i) - let landAt = switch board.fallCoordinates(initialCoords: dropAt, direction: .Bottom) { - case .Border(let at), .Piece(let at, _): - at - case .Occupied: - exit(EXIT_FAILURE) - } - - board[landAt] = Piece(owner: .A) - } +print("All moves:") +for move in rules.validMoves(board: board) { + print(move) } -print(board) +print("Moves for \(Player.A):") +for move in rules.validMoves(board: board, for_player: .A) { + print(move) +} -board[0, 2] = Piece(owner: .B) -board[2, 4] = nil +print(board) -for c in 0..= 3, rows >= 3 else { + public init?(columns: Int, rows: Int, minAligned: Int = 4) { + guard columns >= Self.COLUMNS_MIN, rows >= Self.ROWS_MIN, minAligned > 1 else { return nil } @@ -60,9 +70,9 @@ public struct FourInARowRules: Rules { } public func validMoves(board: Board) -> [Move] { - return [ Player.A, Player.B ].flatMap({ + return Player.allCases.flatMap({ player in self.validMoves(board: board, for_player: player).map({ - action in Move(player: .A, action: action) + action in Move(player: player, action: action) }) }) } @@ -102,7 +112,7 @@ public struct FourInARowRules: Rules { fatalError("Illegal move \(move.action)") } - if countMaxRow(center: pieceCoords, board: board) >= self.minAligned { + if Self.countMaxRow(center: pieceCoords, board: board) >= self.minAligned { self.state = .Finished(winner: move.player) } else if board.countPieces() == board.columns * board.rows { self.state = .Finished(winner: nil) @@ -118,12 +128,12 @@ public struct FourInARowRules: Rules { } } - private func countMaxRow(center: Coords, board: Board) -> Int { + internal static func countMaxRow(center: Coords, board: Board) -> Int { guard let of = board[center]?.owner else { return 0 } var maxLength = 0 // For each "axis" (described as one direction) - for dir: (dc: Int, dr: Int) in [(1, 0), (0, 1), (1, 1)] { + for dir: (dc: Int, dr: Int) in [(1, 0), (0, 1), (1, 1), (-1, 1)] { var length = 1 // Run in the two opposite directions of the axis to sum the length @@ -132,7 +142,7 @@ public struct FourInARowRules: Rules { while true { pos = Coords(pos.col + dc, pos.row + dr) - if board.isInBounds(pos) && board[pos]?.owner != of { break } + if !board.isInBounds(pos) || board[pos]?.owner != of { break } length += 1 } } diff --git a/Model/Sources/Model/Rules/Move.swift b/Model/Sources/Model/Rules/Move.swift index d25c197..01db9b7 100644 --- a/Model/Sources/Model/Rules/Move.swift +++ b/Model/Sources/Model/Rules/Move.swift @@ -1,8 +1,13 @@ -public struct Move { - let player: Player - let action: Action +public struct Move: Equatable { + public let player: Player + public let action: Action - public enum Action { + public init(player: Player, action: Action) { + self.player = player + self.action = action + } + + public enum Action: Equatable { case InsertOnSide(side: Direction, offset: Int) case InsertAt(where: Coords) } diff --git a/Model/Sources/Model/Rules/Rules.swift b/Model/Sources/Model/Rules/Rules.swift index 5efb519..6937d79 100644 --- a/Model/Sources/Model/Rules/Rules.swift +++ b/Model/Sources/Model/Rules/Rules.swift @@ -16,7 +16,7 @@ public protocol Rules { mutating func onMoveDone(move: Move, board: Board) -> Void } -public enum GameState { +public enum GameState: Equatable { case Playing(turn: Player) case Finished(winner: Player?) diff --git a/Model/Tests/ModelTests/EmptyBoardTests.swift b/Model/Tests/ModelTests/EmptyBoardTests.swift index 476fa85..f26b158 100644 --- a/Model/Tests/ModelTests/EmptyBoardTests.swift +++ b/Model/Tests/ModelTests/EmptyBoardTests.swift @@ -5,7 +5,7 @@ final class EmptyBoardTests: XCTestCase { private var board: Board! override func setUpWithError() throws { - super.setUp() + try super.setUpWithError() guard let board = Board(columns: 5, rows: 6) else { XCTFail() diff --git a/Model/Tests/ModelTests/Rules/FourInARowRulesTests.swift b/Model/Tests/ModelTests/Rules/FourInARowRulesTests.swift new file mode 100644 index 0000000..c32c0f7 --- /dev/null +++ b/Model/Tests/ModelTests/Rules/FourInARowRulesTests.swift @@ -0,0 +1,140 @@ +import XCTest +@testable import Model + +final class FourInARowRulesTests: XCTestCase { + private var rules: FourInARowRules! + private var board: Board! + + override func setUpWithError() throws { + try super.setUpWithError() + + guard let rules = FourInARowRules(columns: 3, rows: 3, minAligned: 3) else { + XCTFail() + return + } + + self.rules = rules + self.board = self.rules.createBoard() + } + + func testEmptyHistory() { + XCTAssertTrue(self.rules.history.isEmpty) + } + + func testBoardIsValid() { + XCTAssertTrue(self.rules.isValid(board: self.board)) + } + + func testState() { + XCTAssertEqual(self.rules.state, GameState.Playing(turn: .A)) + } + + func testMovesAreValid() { + for move in self.rules.validMoves(board: self.board) { + rules.state = .Playing(turn: move.player) + XCTAssertTrue(self.rules.isValid(board: board, move: move), "\(move)") + } + } + + func testMovesOfPlayerAreValid() throws { + for player in Player.allCases { + try self.setUpWithError() + + try self._testMovesOfPlayerAreValid(player: player) + + try self.tearDownWithError() + } + } + + private func _testMovesOfPlayerAreValid(player: Player) throws { + for action in self.rules.validMoves(board: self.board, for_player: player) { + rules.state = .Playing(turn: player) + XCTAssertTrue(self.rules.isValid(board: board, move: Move(player: player, action: action)), "\(action)") + } + } + + func testOnMoveDone() { + let coord = Coords(1, board.rows - 1) + board[coord] = Piece(owner: .A) + let move = Move(player: .A, action: .InsertOnSide(side: .Top, offset: 1)) + self.rules.onMoveDone(move: move, board: self.board) + + XCTAssertEqual(self.rules.state, GameState.Playing(turn: .B)) + XCTAssertEqual(self.rules.history.last, move) + } + + func testOnMoveDoneDraw() { + board[0, 0] = Piece(owner: .A) + board[1, 0] = Piece(owner: .A) + board[2, 0] = Piece(owner: .B) + board[0, 1] = Piece(owner: .B) + board[1, 1] = Piece(owner: .B) + board[2, 1] = Piece(owner: .A) + board[0, 2] = Piece(owner: .A) + board[1, 2] = Piece(owner: .A) + + board[2, 2] = Piece(owner: .B) + + XCTAssertTrue(rules.isValid(board: board)) + + let move = Move(player: .B, action: .InsertOnSide(side: .Top, offset: 2)) + + self.rules.onMoveDone(move: move, board: board) + + XCTAssertEqual(rules.state, GameState.Finished(winner: nil)) + } + + func testOnMoveDoneWin() { + board[0, 0] = Piece(owner: .A) + board[1, 0] = Piece(owner: .A) + board[2, 0] = Piece(owner: .A) // + board[0, 1] = Piece(owner: .B) + board[1, 1] = Piece(owner: .B) + board[2, 1] = Piece(owner: .A) + board[0, 2] = Piece(owner: .A) + board[1, 2] = Piece(owner: .A) + board[2, 2] = Piece(owner: .B) + + XCTAssertTrue(rules.isValid(board: board)) + + let move = Move(player: .A, action: .InsertOnSide(side: .Top, offset: 2)) + + self.rules.onMoveDone(move: move, board: board) + + XCTAssertEqual(rules.state, GameState.Finished(winner: .A)) + } + + func testCountMaxRow() throws { + for (coords, expected) in [ + (Coords(1, 0), 3), + (Coords(0, 1), 2), + (Coords(0, 2), 2), + (Coords(1, 1), 3), + (Coords(2, 2), 3), + (Coords(2, 1), 2), + ] { + try self.setUpWithError() + + // AAA + // BAB + // BBA + board[0, 0] = Piece(owner: .A) + board[1, 0] = Piece(owner: .A) + board[2, 0] = Piece(owner: .A) + board[0, 1] = Piece(owner: .B) + board[1, 1] = Piece(owner: .A) + board[2, 1] = Piece(owner: .B) + board[0, 2] = Piece(owner: .B) + board[1, 2] = Piece(owner: .B) + board[2, 2] = Piece(owner: .A) + + self._testCountMaxRow(coords: coords, expected: expected) + + try self.tearDownWithError() + } + } + + private func _testCountMaxRow(coords: Coords, expected: Int) { + XCTAssertEqual(FourInARowRules.countMaxRow(center: coords, board: board), expected, "\(coords)") + } +}