// 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
0 B
|👍
/👎