SwiftUI 井字棋盘

SwiftUI TicTacToe board

我是 SwiftUI 的新手,我正在为我的 class 开发 TicTacToe 棋盘。我正在关注 Medium 上的 this 文章,但我遇到了问题。

你玩的时候方块不会激活。直到游戏结束,您才能看到移动的位置。 我不明白为什么会这样或如何解决它。任何帮助将不胜感激!

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

代码中有一些小的调整。这基本上是数据结构及其通信方式的错误。将它与您的代码进行比较,看看有什么不同。此外,aiMove 并不是真正的人工智能。实际上它是一个随机移动而不是极小极大算法。

import SwiftUI
import Combine

enum SquareStatus {
    case empty
    case visitor
    case home
}

struct Square {
    var status: SquareStatus
}

class ModelBoard: ObservableObject {
    @Published 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 {
            var square = squares[index]
            square.status = player
            squares[index] = square
            if player == .home { aiMove() }
            return true
        }
        return false
    }
}



struct SquareView: View {
    var dataSource: Square
    var action: () -> Void
    var body: some View {
        Button(action: {
            print(self.dataSource.status)
            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 MainBoard: View {
    @ObservedObject 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()
                        }) )
        }
    }
}

您可能希望将视图模型作为环境对象传递到视图层次结构中,而不是在视图中对 VM 的构造进行硬编码。我稍微清理了一下代码。以下代码在 XCode 12.5.

上运行良好

我无法使用@NikzJon 版本或 Costantino Pistagna 的版本来处理最新的 XCode。

完整的项目可以在 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: [=10=])})
    static let cols : [WinCondition] = Array((0...2).map { col(x: [=10=])})
    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 { [=10=].element == .vacant }
           .map { [=10=].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 { [=10=].line.allSatisfy { squares[[=10=]] == 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)
        }
    }
}