diff --git a/CLI/CLI/main.swift b/CLI/CLI/main.swift index 5296564..2c88e62 100644 --- a/CLI/CLI/main.swift +++ b/CLI/CLI/main.swift @@ -3,92 +3,93 @@ import Foundation import Model import CustomTypes -let ai = RandomPlayer(name: "Le robot a mythos", piece_type: .A) -let cli = HumanPlayer(name: "Humain", piece_type: .B, callback: { - allowed_moves, board in - print("Available moves:") - for i in allowed_moves.indices { - print("\(i + 1)\t\(allowed_moves[i])") - } - print(">") - - guard let input = readLine(strippingNewline: true) else { - fatalError("Invalid input") +enum EnumRules: String, CaseIterable { + case FourInARow = "Four in a row" + case TicTacToe = "Tic Tac Toe" +} + +func enumPrompt & CaseIterable>(prompt: String) -> E where E.AllCases.Index == Int { + print("\(prompt):") + E.allCases.enumerated().forEach { (index, variant) in + print("\t\(index + 1). \(variant.rawValue)") } - guard let id = Int(input), id >= 1, id <= allowed_moves.count else { - fatalError("Invalid move id: \(input)") + guard let rawInput = readLine(strippingNewline: true), + let index = Int(rawInput), index >= 1, index <= E.allCases.count + else { + fatalError("Invalid variant number") } - return allowed_moves[id - 1] -}) - -let players = [ cli, ai ] - -guard var rules = FourInARowRules( - columns: FourInARowRules.COLUMNS_DEFAULT, - rows: FourInARowRules.ROWS_DEFAULT, - players: players -) else { - fatalError("Rules settings are invalid") + return E.allCases[index - 1] } -var board = rules.createBoard() +let gameType: EnumRules = enumPrompt(prompt: "Choose a game type") -guard rules.isValid(board: board) else { - fatalError("Board is invalid") +enum PlayerType: String, CaseIterable { + case AIRandom = "(AI) Random" + case Human = "Player" } -var state = rules.gameState(board: board, last_turn: nil) -print(state) +func createPlayer(type: PlayerType, playing pieces: PieceType, named name: String) -> Player { + switch type { + case .Human: + HumanPlayer(name: name, piece_type: pieces, callback: { + allowed_moves, board in + for (i, action) in allowed_moves.enumerated() { + let text = switch action { + case .InsertAt(let at): "Place piece at \(at.col):\(at.row)" + case .InsertOnSide(let side, let offset): "Fall piece from \(side), offset \(offset)" + } + print("\(i + 1). \(text)") + } + + guard let rawInput = readLine(strippingNewline: true), + let index = Int(rawInput), index >= 1, index <= allowed_moves.count + else { + fatalError("Invalid move number") + } + + return allowed_moves[index - 1] + }) + case .AIRandom: + RandomPlayer(name: name, piece_type: pieces) + } +} -print("All moves:") -for move in rules.validMoves(board: board) { - print(move) +let players: [Player] = PieceType.allCases.map { type in + let ptype: PlayerType = enumPrompt(prompt: "Player playing pieces \(type)") + + print("Enter player name:") + guard let playerName = readLine(strippingNewline: true) else { + fatalError("Player name unspecified") + } + + return createPlayer(type: ptype, playing: type, named: playerName) } -print("Moves for \(PieceType.A):") -for move in rules.validMoves(board: board, for_player: ai) { - print(move) +let rules: Rules = switch gameType { +case .FourInARow: FourInARowRules(players: players)! +case .TicTacToe: TicTacToeRules(players: players)! } -print(board) +var game = Game(players: players, rules: rules) -while case .Playing(let turn) = state { - let action = turn.chooseMove( - allowed_moves: rules.validMoves(board: board, for_player: turn), - board: board - ) - - let move = Move(player: turn, action: action) - guard rules.isValid(board: board, move: move) else { - fatalError("Move of \(turn) is invalid!") - } - - switch move.action { - case .InsertAt(let coords): - board[coords] = Piece(type: move.player.piece_type) - case .InsertOnSide(let side, let offset): - switch board.fallCoordinates( - initialCoords: board.getInsertionCoordinates(from: side, offset: offset), - direction: side.opposite - ) { - case .Border(let at), .Piece(let at, _): - board[at] = Piece(type: move.player.piece_type) - case .Occupied: - fatalError("Not supported") - } - } - +game.boardChanged.add { board in print(board) - - state = rules.gameState(board: board, last_turn: turn) } - -if case .Win(let winner, _) = state { - print("\(winner.name) is the winner") -} else { - print("Draw.") +game.moveIsInvalid.add { move in + print("\(move.player.name), this move is invalid according to the rules.") +} +game.gameStateChanged.add { state in + switch state { + case .Playing(let player): + print("\(player.name) turn.") + case .Win(let winner, let board, let cells): + print("\(winner.name) is the winner") + print(board.display(winCells: cells)) + case .Draw: + print("Its a daw.") + } } -exit(EXIT_SUCCESS) +game.play() diff --git a/CustomTypes/Sources/CustomTypes/Display.swift b/CustomTypes/Sources/CustomTypes/Display.swift index 3b2b6a0..45ff281 100644 --- a/CustomTypes/Sources/CustomTypes/Display.swift +++ b/CustomTypes/Sources/CustomTypes/Display.swift @@ -1,23 +1,21 @@ import Model -extension PieceType: CustomStringConvertible { - public var description: String { - switch self { - case .A: - "🔴" - case .B: - "🟡" - } - } -} - -extension Board: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { +extension Board { + public func display(winCells: [Coords]) -> String { var str = String() for row in 0.. Void +public typealias GameStateChangedListener = (GameState) -> Void + +public class Game { + public var boardChanged = Event() + public var gameStateChanged = Event() + public var moveIsInvalid = Event() + + public let players: [Player] + public let rules: Rules + + public init(players: [Player], rules: Rules) { + self.players = players + self.rules = rules + } + + public func play() { + var board = rules.createBoard() + var state = rules.gameState(board: board, last_turn: nil) + + boardChanged(board) + gameStateChanged(state) + + while case .Playing(let player_turn) = state { + let valid_moves = rules.validMoves(board: board, for_player: player_turn) + + if valid_moves.isEmpty { + player_turn.skipMove(board: board) + } else { + while true { + let choosen_action = player_turn.chooseMove(allowed_moves: valid_moves, board: board) + let move = Move(player: player_turn, action: choosen_action) + + guard rules.isValid(board: board, move: move) else { + moveIsInvalid(move) + continue + } + + self.performMove(move: move, on_board: &board) + precondition(rules.isValid(board: board), "move that was valid made the board invalid") + + boardChanged(board) + + break + } + } + + state = rules.gameState(board: board, last_turn: player_turn) + gameStateChanged(state) + } + + // Finished + } + + private func performMove(move: Move, on_board board: inout Board) { + let finalCoords = switch move.action { + case .InsertAt(let at): at + + case .InsertOnSide(let side, let offset): + { + // TODO: with this, I am unsure how other moves on sides should handle pushing, popping, ... + let insertCoords = board.getInsertionCoordinates(from: side, offset: offset) + return switch board.fallCoordinates(initialCoords: insertCoords, direction: side.opposite) { + case .Border(let at), .Piece(let at, _): + at + case .Occupied: + // NOTE: assure the move is indeed legal and replace the current piece + insertCoords + } + }() + } + + board[finalCoords] = Piece(owner: move.player) + } +} + +@dynamicCallable +public struct Event { + private var listeners: [(Param) -> Void] = [] + + public init() {} + + public mutating func add(_ listener: @escaping (Param) -> Void) { + self.listeners.append(listener) + } + + // func dynamicallyCall(withArguments args: [Int]) -> Int { return args.count } + + func dynamicallyCall(withArguments args: [Param]) -> Void { + for param in args { + for listener in self.listeners { + listener(param) + } + } + } +} diff --git a/Model/Sources/Model/Piece.swift b/Model/Sources/Model/Piece.swift index f5c1882..dd097b2 100644 --- a/Model/Sources/Model/Piece.swift +++ b/Model/Sources/Model/Piece.swift @@ -1,8 +1,8 @@ -public struct Piece: Equatable, Sendable { - public let type: PieceType +public struct Piece: Equatable { + public let owner: Player + public var type: PieceType { owner.piece_type } - // Required for public visibility - public init(type: PieceType) { - self.type = type + public init(owner: Player) { + self.owner = owner } } diff --git a/Model/Sources/Model/Players/Player.swift b/Model/Sources/Model/Players/Player.swift index a58b7e3..58e90cb 100644 --- a/Model/Sources/Model/Players/Player.swift +++ b/Model/Sources/Model/Players/Player.swift @@ -11,6 +11,11 @@ public class Player : Equatable { fatalError("abstract method not implemented") } + /// Called when there is no possible move for this player's turn + public func skipMove(board: Board) -> Void { + // NO-OP + } + public static func == (lhs: Player, rhs: Player) -> Bool { // TODO: name equality or reference equality? lhs === rhs diff --git a/Model/Sources/Model/Rules/FourInARowRules.swift b/Model/Sources/Model/Rules/FourInARowRules.swift index 988c10b..e893463 100644 --- a/Model/Sources/Model/Rules/FourInARowRules.swift +++ b/Model/Sources/Model/Rules/FourInARowRules.swift @@ -112,7 +112,7 @@ public struct FourInARowRules: Rules { if cells.count >= minimum_aligned { let player = players.first(where: { $0.piece_type == piece.type })! - return GameState.Win(winner: player, cells: cells) + return GameState.Win(winner: player, board: board, cells: cells) } } } diff --git a/Model/Sources/Model/Rules/Rules.swift b/Model/Sources/Model/Rules/Rules.swift index 22e5e71..ab3aad8 100644 --- a/Model/Sources/Model/Rules/Rules.swift +++ b/Model/Sources/Model/Rules/Rules.swift @@ -21,7 +21,7 @@ public protocol Rules { public enum GameState: Equatable { case Playing(turn: Player) - case Win(winner: Player, cells: [Coords]) + case Win(winner: Player, board: Board, cells: [Coords]) case Draw } diff --git a/Model/Sources/Model/Rules/TicTacToeRules.swift b/Model/Sources/Model/Rules/TicTacToeRules.swift index a648abe..e3050c7 100644 --- a/Model/Sources/Model/Rules/TicTacToeRules.swift +++ b/Model/Sources/Model/Rules/TicTacToeRules.swift @@ -2,7 +2,13 @@ public struct TicTacToeRules: Rules { private let players: [Player] private let columns: Int, rows: Int, minAligned: Int - public init(columns: Int, rows: Int, minAligned: Int = 3, players: [Player]) { + public init?(players: [Player]) { + self.init(columns: 3, rows: 3, players: players) + } + + public init?(columns: Int, rows: Int, minAligned: Int = 3, players: [Player]) { + guard players.count >= 2 else { return nil } + self.columns = columns self.rows = rows self.minAligned = minAligned diff --git a/Model/Tests/ModelTests/EmptyBoardTests.swift b/Model/Tests/ModelTests/EmptyBoardTests.swift index 2330321..edd2d82 100644 --- a/Model/Tests/ModelTests/EmptyBoardTests.swift +++ b/Model/Tests/ModelTests/EmptyBoardTests.swift @@ -2,6 +2,9 @@ import XCTest @testable import Model final class EmptyBoardTests: XCTestCase { + private static let playerA = Player(name: "dummy A", piece_type: .A) + private static let playerB = Player(name: "dummy B", piece_type: .B) + private var board: Board! override func setUpWithError() throws { @@ -35,7 +38,7 @@ final class EmptyBoardTests: XCTestCase { for p: PieceType in [.A, .B] { try self.setUpWithError() - try self._testCustomSubscript(at: Coords(c, r), player: p) + try self._testCustomSubscript(at: Coords(c, r), type: p) try self.tearDownWithError() } @@ -43,16 +46,17 @@ final class EmptyBoardTests: XCTestCase { } } - private func _testCustomSubscript(at: Coords, player: PieceType) throws { - board[at] = Piece(type: player) + private func _testCustomSubscript(at: Coords, type: PieceType) throws { + let player = Player(name: "dummy", piece_type: type) + board[at] = Piece(owner: player) - XCTAssertEqual(board[at]?.type, player) + XCTAssertEqual(board[at]?.owner, player) } func testCounts() throws { - board[1, 2] = Piece(type: .B) - board[0, 2] = Piece(type: .A) - board[1, 1] = Piece(type: .A) + board[1, 2] = Piece(owner: Self.playerB) + board[0, 2] = Piece(owner: Self.playerA) + board[1, 1] = Piece(owner: Self.playerA) XCTAssertEqual(board.countPieces { piece in piece.type == .A }, 2) XCTAssertEqual(board.countPieces { piece in piece.type == .B }, 1) @@ -129,9 +133,9 @@ final class EmptyBoardTests: XCTestCase { ] { // ensure it works with any player board[Coords(pair: pos)] = if (pos.0 + pos.1) & 1 == 1 { - Piece(type: .A) + Piece(owner: Self.playerA) } else { - Piece(type: .B) + Piece(owner: Self.playerB) } } diff --git a/Model/Tests/ModelTests/FilledBoardTests.swift b/Model/Tests/ModelTests/FilledBoardTests.swift index c665aec..6ee4273 100644 --- a/Model/Tests/ModelTests/FilledBoardTests.swift +++ b/Model/Tests/ModelTests/FilledBoardTests.swift @@ -2,6 +2,9 @@ import XCTest @testable import Model final class FilledBoardTests: XCTestCase { + private static let playerA = Player(name: "dummy A", piece_type: .A) + private static let playerB = Player(name: "dummy B", piece_type: .B) + private var board: Board! override func setUp() { @@ -15,9 +18,9 @@ final class FilledBoardTests: XCTestCase { for row in 0..