// assumption for Move struct struct Move { var player: Player? var position: Int? var boardIndex: Int = 0 var indicator: String { if let p = player { return p == .human ? "x" : "o" } return "" } } // assumption for AlertItem struct struct AlertItem { var title: String = "Result" var message: String = "message" var buttonTitle: String = "OK" } enum Player { case human case computer } class CodeTicTacToeViewController: UIViewController { var gameBoardView: UIView = { let v = UIView() v.backgroundColor = .systemBlue return v }() var buttons: [UIButton] = [] var viewModel = TicTacToeViewModel() override func viewDidLoad() { super.viewDidLoad() configureUI() let outerStack: UIStackView = { let v = UIStackView() v.axis = .vertical v.spacing = 8 v.distribution = .fillEqually return v }() for _ in 1...3 { let rowStack: UIStackView = { let v = UIStackView() v.axis = .horizontal v.spacing = 8 v.distribution = .fillEqually return v }() for _ in 1...3 { let b: UIButton = { let v = UIButton() v.backgroundColor = .systemYellow return v }() rowStack.addArrangedSubview(b) buttons.append(b) } outerStack.addArrangedSubview(rowStack) } outerStack.translatesAutoresizingMaskIntoConstraints = false gameBoardView.translatesAutoresizingMaskIntoConstraints = false gameBoardView.addSubview(outerStack) view.addSubview(gameBoardView) let g = view.safeAreaLayoutGuide NSLayoutConstraint.activate([ outerStack.topAnchor.constraint(equalTo: gameBoardView.topAnchor, constant: 8.0), outerStack.leadingAnchor.constraint(equalTo: gameBoardView.leadingAnchor, constant: 8.0), outerStack.trailingAnchor.constraint(equalTo: gameBoardView.trailingAnchor, constant: -8.0), outerStack.bottomAnchor.constraint(equalTo: gameBoardView.bottomAnchor, constant: -8.0), gameBoardView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0), gameBoardView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0), gameBoardView.centerYAnchor.constraint(equalTo: g.centerYAnchor), gameBoardView.heightAnchor.constraint(equalTo: gameBoardView.widthAnchor), ]) // set tag and action for the buttons var t: Int = 1 buttons.forEach { b in b.tag = t b.addTarget(self, action: #selector(buttonClicked(_:)), for: .touchUpInside) t += 1 } } func configureUI() { viewModel.delegate = self } @IBAction func buttonClicked(_ sender: UIButton) { viewModel.processPlayerMove(for: sender.tag) } /// This **function is to reset the view means remove close and circle image once player wants to restart the game. But this code is not working** /// should be working now func updateButtonView() { DispatchQueue.main.async { for (index, moves) in self.viewModel.moves.enumerated() { let button = self.buttons[index] if let moves = moves { // let's make sure we get a valid image if let image = UIImage(named: moves.indicator) { button.setImage(image, for: []) button.setTitle("", for: []) } else { button.setImage(nil, for: []) button.setTitle(moves.indicator, for: []) } } else { button.setImage(nil, for: []) button.setTitle("", for: []) } } } } } /// Delegates are called from viewmodel extension CodeTicTacToeViewController: TicTacToeViewModelProtocol { func updatePlayerInfo(index: Int) { let button = buttons[index - 1] let systemImageName = viewModel.moves[index - 1]?.indicator ?? "" if !systemImageName.isEmpty { // let's make sure we get a valid image if let image = UIImage(named: systemImageName) { button.setImage(image, for: []) button.setTitle("", for: []) } else { button.setImage(nil, for: []) button.setTitle(systemImageName, for: []) } } else { button.setImage(nil, for: []) button.setTitle("", for: []) } } func displayAlert(alertData: AlertItem) { let alert = UIAlertController(title: alertData.title, message: alertData.message, preferredStyle: UIAlertController.Style.alert) alert.addAction(UIAlertAction(title: alertData.buttonTitle, style: UIAlertAction.Style.default, handler: { [weak self] _ in self?.viewModel.resetGame() self?.updateButtonView() })) self.present(alert, animated: true, completion: nil) } func gameBoardDisabled(isGameBoardDisable: Bool) { gameBoardView.isUserInteractionEnabled = !isGameBoardDisable } } protocol TicTacToeViewModelProtocol: AnyObject { func updatePlayerInfo(index: Int) func displayAlert(alertData: AlertItem) func gameBoardDisabled(isGameBoardDisable: Bool) } class TicTacToeViewModel { var currentMovePosition: Int? var moves: [Move?] = Array(repeating: nil, count: 9) { didSet { if let delegate = delegate, let position = self.currentMovePosition { delegate.updatePlayerInfo(index: position) } } } var isGameBoardDisable = false { didSet { if let delegate = delegate { delegate.gameBoardDisabled(isGameBoardDisable: isGameBoardDisable) } } } var alertItem: AlertItem? { didSet { if let delegate = delegate, let alertItem = alertItem { delegate.displayAlert(alertData: alertItem) } } } let winPatterns: Set> = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [1, 4, 7], [2, 5, 8], [3, 6, 9], [1, 5, 9], [3, 5, 7]] weak var delegate: TicTacToeViewModelProtocol! func processPlayerMove(for position: Int) { self.currentMovePosition = position if isSquareOccupied(in: moves, forIndex: position) { return } moves[position - 1] = Move(player: .human, boardIndex: position) // logic for win, draw or lose if checkWinCondition(for: .human, in: moves) { var a = AlertItem() a.message = "Human Wins" alertItem = a return } if checkForDraw(in: moves) { var a = AlertItem() a.message = "Player Draw" alertItem = a return } isGameBoardDisable = true processComputerMove() } func processComputerMove() { let computerPosition = determinComputerMovePosition(in: moves) self.currentMovePosition = computerPosition moves[computerPosition - 1] = Move(player: .computer, boardIndex: computerPosition) isGameBoardDisable = false if checkWinCondition(for: .computer, in: moves) { var a = AlertItem() a.message = "Computer Wins" alertItem = a return } if checkForDraw(in: moves) { var a = AlertItem() a.message = "Computer Draw" alertItem = a return } } func isSquareOccupied(in moves:[Move?], forIndex index: Int) -> Bool { return moves.contains(where: { $0?.boardIndex == index }) } func determinComputerMovePosition(in moves: [Move?]) -> Int { let computerMoves: [Move] = moves.compactMap { $0 }.filter { $0.player == .computer } let computerPositions: Set = Set(computerMoves.map { $0.boardIndex }) for pattern in winPatterns { let winPositions = pattern.subtracting(computerPositions) if winPositions.count == 1 { let isAvailable = !isSquareOccupied(in: moves, forIndex: winPositions.first!) if isAvailable { return winPositions.first! } } } // if the AI ​​can't finish the game it will block let humanMoves = moves.compactMap { $0 }.filter { $0.player == .human } let humanPositions = Set(humanMoves.map { $0.boardIndex }) for pattern in winPatterns { let winPositions = pattern.subtracting(humanPositions) if winPositions.count == 1 { let isAvailable = !isSquareOccupied(in: moves, forIndex: winPositions.first!) if isAvailable { return winPositions.first! } } } // always take the middle block let middleSquare = 5 if !isSquareOccupied(in: moves, forIndex: middleSquare) { return middleSquare } // this could theoretically take a LONG time to process! // if the AI ​​can't get position middle block it will get a random position //var movePosition = Int.random(in: 1..<10) //while isSquareOccupied(in: moves, forIndex: movePosition) { // movePosition = Int.random(in: 1..<10) //} //return movePosition // create a shuffled array of 1 to 9 // loop through until we find an available position var positions: [Int] = Array(1...9).shuffled() var movePosition: Int = positions.removeFirst() while isSquareOccupied(in: moves, forIndex: movePosition) { movePosition = positions.removeFirst() } return movePosition } func checkWinCondition(for player: Player, in moves:[Move?]) -> Bool { let playerMoves = moves.compactMap({ $0 }).filter { $0.player == player } let playerPositions = Set(playerMoves.map { $0.boardIndex }) for pattern in winPatterns where pattern.isSubset(of: playerPositions) {return true} return false } func checkForDraw(in moves: [Move?]) -> Bool { return moves.compactMap { $0 }.count == 9 } func resetGame() { moves = Array(repeating: nil, count: 9) } }