Don_Mag icon

tic tac toe

Don_Mag | PRO | 02/28/22 12:50:16 PM UTC (Edited) | 0 ⭐ | 1969 👁️ | Never ⏰ | []
Swift |

9.02 KB

|

None

|

0 👍

/

0 👎

// 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<Set<Int>> = [[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<Int> = 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)
    }
}
 

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎