Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI TicTacToe board

Tags:

I'm very new to SwiftUI, and I'm working on a TicTacToe board for my class. I'm following this article on Medium, but I've encountered a problem.

The squares don't activate as you play. It's not until the game is finished that you can see where the moves were made. I don't understand why this is happening or how to fix it. Any help would be much appreciated!

import SwiftUI
import Combine

enum SquareStatus {
    case empty
    case visitor
    case home
}

class Square: ObservableObject {
    let didChange = PassthroughSubject<Void, Never>()

    var status: SquareStatus {
        didSet {
            didChange.send(())
        }
    }

    init(status: SquareStatus) {
        self.status = status
    }
}

class ModelBoard {
    var squares = [Square]()
    init() {
        for _ in 0...8 {
            squares.append(Square(status: .empty))
        }
    }
    func resetGame() {
        for i in 0...8 {
            squares[i].status = .empty
        }
    }
    var gameOver: (SquareStatus, Bool) {
        get {
            if thereIsAWinner != .empty {
                return (thereIsAWinner, true)
            } else {
                for i in 0...8 {
                    if squares[i].status == .empty {
                        return (.empty, false)
                    }
                }
                return (.empty, true)
            }
        }
    }
    private var thereIsAWinner:SquareStatus {
        get {
            if let check = self.checkIndexes([0, 1, 2]) {
                return check
            } else  if let check = self.checkIndexes([3, 4, 5]) {
                return check
            }  else  if let check = self.checkIndexes([6, 7, 8]) {
                return check
            }  else  if let check = self.checkIndexes([0, 3, 6]) {
                return check
            }  else  if let check = self.checkIndexes([1, 4, 7]) {
                return check
            }  else  if let check = self.checkIndexes([2, 5, 8]) {
                return check
            }  else  if let check = self.checkIndexes([0, 4, 8]) {
                return check
            }  else  if let check = self.checkIndexes([2, 4, 6]) {
                return check
            }
            return .empty
        }
    }
    private func checkIndexes(_ indexes: [Int]) -> SquareStatus? {
        var homeCounter:Int = 0
        var visitorCounter:Int = 0
        for anIndex in indexes {
            let aSquare = squares[anIndex]
            if aSquare.status == .home {
                homeCounter = homeCounter + 1
            } else if aSquare.status == .visitor {
                visitorCounter = visitorCounter + 1
            }
        }
        if homeCounter == 3 {
            return .home
        } else if visitorCounter == 3 {
            return .visitor
        }
        return nil
    }
    private func aiMove() {
        var anIndex = Int.random(in: 0 ... 8)
        while (makeMove(index: anIndex, player: .visitor) == false && gameOver.1 == false) {
            anIndex = Int.random(in: 0 ... 8)
        }
    }
    func makeMove(index: Int, player:SquareStatus) -> Bool {
        if squares[index].status == .empty {
            squares[index].status = player
            if player == .home { aiMove() }
            return true
        }
        return false
    }
}

struct SquareView: View {
    @ObservedObject var dataSource:Square
    var action: () -> Void
    var body: some View {
        Button(action: {
            self.action()
        }) {
            Text((dataSource.status != .empty) ?
                (dataSource.status != .visitor) ? "X" : "0"
                : " ")
                .font(.largeTitle)
                .foregroundColor(Color.black)
                .frame(minWidth: 60, minHeight: 60)
                .background(Color.gray)
                .padding(EdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4))
        }
    }
}

struct ContentView : View {
    private var checker = ModelBoard()
    @State private var isGameOver = false

    func buttonAction(_ index: Int) {
        _ = self.checker.makeMove(index: index, player: .home)
        self.isGameOver = self.checker.gameOver.1
    }
    var body: some View {
        VStack {
            HStack {
                SquareView(dataSource: checker.squares[0]) { self.buttonAction(0) }
                SquareView(dataSource: checker.squares[1]) { self.buttonAction(1) }
                SquareView(dataSource: checker.squares[2]) { self.buttonAction(2) }
            }
            HStack {
                SquareView(dataSource: checker.squares[3]) { self.buttonAction(3) }
                SquareView(dataSource: checker.squares[4]) { self.buttonAction(4) }
                SquareView(dataSource: checker.squares[5]) { self.buttonAction(5) }
            }
            HStack {
                SquareView(dataSource: checker.squares[6]) { self.buttonAction(6) }
                SquareView(dataSource: checker.squares[7]) { self.buttonAction(7) }
                SquareView(dataSource: checker.squares[8]) { self.buttonAction(8) }
            }
            }
        .alert(isPresented: $isGameOver) {
                Alert(title: Text("Game Over"),
                      message: Text(self.checker.gameOver.0 != .empty ?
                        (self.checker.gameOver.0 == .home) ? "You Win!" : "iPhone Wins!"
                        : "Parity"), dismissButton: Alert.Button.destructive(Text("Ok"), action: {
                            self.checker.resetGame()
                        }) )
        }
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif
like image 439
alison Avatar asked May 05 '20 20:05

alison


1 Answers

You probably want to pass your View Model into the view hierarchy as an Environment Object instead of hard coding the construction of the VM in the View. I cleaned up the code a bit. The following code works fine on XCode 12.5.

I couldn't get @NikzJon version or Costantino Pistagna's version to work on the latest XCode.

The full project can be found at GitHub

//  ContentView.swift

import SwiftUI

enum Owner
{
    case vacant
    case naught
    case cross
    
    var show: String
    {
        get {
            switch self
            {
            case .vacant:
                return " "
            case .naught:
                return "0"
            case .cross:
                return "X"
            }
        }
    }
    var showWinnerAsCross : String
    {
        switch self
        {
        case .vacant:
            return "Draw!"
        case .naught:
            return "Lose!"
        case .cross:
            return "Win!"
        }
    }
    var showWinnerAsNaught : String
    {
        switch self
        {
        case .vacant:
            return "Draw!"
        case .cross:
            return "Lose!"
        case .naught:
            return "Win!"
        }
    }
    static func blankSquares(_ n: Int) -> [Owner]
    {
       return [Owner](repeating: .vacant, count: n)
    }
}
enum Slant
{
    case forward
    case back
}
enum WinCondition
{
    case row (y:Int)
    case col  (x: Int)
    case diag (slant: Slant)
    
    static let rows : [WinCondition] = Array((0...2).map { row(y: $0)})
    static let cols : [WinCondition] = Array((0...2).map { col(x: $0)})
    static let diags : [WinCondition] = [diag(slant: .forward), diag(slant: .back)]
    
    static let all : [WinCondition] =  rows + cols + diags
    
    var line : [Int] { get {
        switch self{
        case  .row(y: let y) :
            return [3*y+0,3*y+1,3*y+2]
        case .col(x: let x):
            return [x+0,x+3,x+6]
        case .diag(slant: let slant):
            return slant == .forward ? [0,4,8] : [2,4,6]
        }
    }
    }
    
}
 extension Array where Element == Owner
 {
    var free:  [Int]  { get
       {
       self
           .enumerated()
           .filter { $0.element == .vacant }
           .map { $0.offset }
       }
    }
    var randomFreeIndex:   Int   { get
       {
        let moves =  self.free
        
        let move = Int.random(in: 0..<moves.count)
        return moves[move]
       }
    }
    var isFull:   Bool   {
        get
       {
            return self.free.isEmpty
       }
    }
 }
class GameBoard : ObservableObject
{
    @Published var squares =  Owner.blankSquares( 9)
 
    var currentPlayer = Owner.cross
    var computerPlayer = Owner.naught
    
    func reset() {
          squares = Owner.blankSquares( 9)
     }
     
   
    func hasWon(player: Owner) -> Bool {
        return WinCondition.all.contains { $0.line.allSatisfy { squares[$0] == player }}
    }
    var winner : Owner?
    {
        get {
            if hasWon(player: .cross)
            {
                return .cross
            }
            else if hasWon(player: .naught)
            {
                return .naught
            }
            else if squares.free.isEmpty
            {
            return .vacant
            }
            return nil
        }
    }
     
    @Published var isOver : Bool = false;
    func checkIsOver()
    {
        if winner != nil
        {
            isOver = true
        }
    }
    var showWinner : String
    {
        get{
            return  "You " + (winner?.showWinnerAsCross ?? " ")
        }
    }
    func randomMove(player: Owner)    {
        if squares.isFull
        {
            return
        }
        squares[squares.randomFreeIndex] = player
    }
    func turn(squareIndex:Int) {
        if  squares[squareIndex] != .vacant
        {
            return
        }
        squares[squareIndex] =  currentPlayer
        checkIsOver()
        randomMove(player: computerPlayer)
        checkIsOver()
    }

}
struct SquareView: View {
    @EnvironmentObject var board: GameBoard
    var index: Int
    var body: some View {
        Button(action: {
            board.turn(squareIndex: index)
        }) {
            Text( board.squares[index].show)
                .foregroundColor(Color.white)
                .font(.largeTitle)
                .frame(minWidth: 80, minHeight: 80)
                .background(Color.blue)
                .padding(EdgeInsets(top: 3, leading: 3, bottom: 3, trailing: 3))
        }
    }
}
struct ContentView: View {
     @EnvironmentObject var board: GameBoard
  
      var body: some View {
          VStack {
            HStack {
              SquareView(index:0)
              SquareView(index:1)
              SquareView(index:2)
            }
            HStack {
              SquareView(index:3)
              SquareView(index:4)
              SquareView(index:5)
            }
            HStack {
              SquareView(index:6)
              SquareView(index:7)
              SquareView(index:8)
            }
            }
          .alert(isPresented: $board.isOver) {
                  Alert(title: Text( board.showWinner) ,  dismissButton: Alert.Button.destructive(Text("Try Again"), action:
                              board.reset
                           ) )
          }
      }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

//  tictactoeApp.swift


import SwiftUI

@main
struct tictactoeApp: App {
    @StateObject var board = GameBoard()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(board)
        }
    }
}
like image 98
Geoff Burns Avatar answered Oct 21 '22 05:10

Geoff Burns