MorpionView est sur l'Apple Store

Bonjour heureux de vous annoncer la mise sur l’apple Store de mon Application TicTacToe
possibilité de jouer avec un damier de 3X3 à 6X6 entièrement fait en SwitUI
vous jouez contre l’ordinateur avec 3 niveaux de difficultés
il y a des bugs je ne gère pas le mode paysage
merci de me faire part de vos remarques

c’est ma 6eme App avec
Toi ou Moi gestion de dépenses liaison avec l’appleWatch
Belote Calc, Tarots Calc
MgTipcalculor et devises convertisseurs qui fonctionnent sur AppleWatch
à bientôt

3 « J'aime »

Le mode paysage est désactivé pour mon iPhone 8, par contre il est présent sur l’iPad Pro. Et ce n’est pas bien joli !

Je suis étonné que l’application soit passé la validation, sachant que la plupart des jeux sont en mode paysage sur l’iPad. Les lutins doivent être très fatigués à cause du confinement …

Sinon ça se bloque1 fois sur deux quand le joueur perd la partie (sur mon iPhone 8). Il faut alors « tuer » l’application et la relancer pour jouer.

Copie d’écran d’un blocage :

Je ne sais pas si c’est un plantage de l’application, ou un blocage de l’interface. Plus rien ne fonctionne. Tous les contrôles graphiques sont inertes, ne réagissant plus. La seule manière de s’en sortir c’est de quitter l’application et de la tuer.

Il y a d’autres choses à dire sur l’ergonomie et la présentation. Mais c’est annexe par rapport à ce blocage.

iPhone 8
iOS 13.4.1

EDIT : Le blocage de l’interface semble se produire en cliquant sur une case vide du jeu APRES que la partie soit terminée.

J’ai corrigé le mode paysage sur iPad
Je vais essayer de trouver pour le blocage
merci pour retour

j’ai bien désactivé le mode paysage sur le projet
mais çà ne le désactive pas sur l’iPad ? seulement sur les iPhones

Effectivement çà coince
j’ai mis le source sur GitHub

j’ai fait une correction sur l’apple store 1.03 qui est en attente de validation
qui semble corriger le problème

C’est (presque) bon pour le mode paysage sur l’iPad. Cela semble fonctionner mais la géométrie de l’image est très légèrement déformée.

Les cases, parfaitement symétriques en mode portrait ont une taille de 178x150 pixels en orientation paysage, ce qui déforme légèrement les images. C’est subtilement dérangeant.


Le blocage vient certainement de cette partie du code :

                            if self.gameIsActive && self.quiJoue == true {
                                DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
                                    var trouve = false

Tu demandes à l’application d’exécuter quelque chose après un délai d’une seconde, pour donner l’impression que l’IA « pense ». il doit y avoir une erreur dans les conditions de tests, qui lance l’attente si l’utilisateur touche une case après la fin du jeu. Cela boucle peut-être lançant l’exécution d’une multitude de d’attente d’une seconde. Commence par supprimer ce délai d’attente pour voir si cela agit sur le blocage


Sinon ton code est un vrai spaghetti. Les lignes de code partent dans tous les sens, et la logique du jeu est incluse dans le code des boutons de l’interface. Pas facile à suivre l’écoulement du flux programmatique, avec cette structure et ces tests dans toutes les directions.

J’aurais procédé différemment en dissociant l’interface, la gestion des cases et le traitement des événements. Regarde cet exemple :

Il y a 4 cases sur l’écran, identifiée par un Tag. Une classe MoteurDeJeu() sert à gérer le jeu. Chaque fois que le joueur presse sur une case, le moteurDeJeu() est prévenu par une fonction. Cela permet de séparer la partie graphique du traitement des événements, et d’avoir un code propre, lisible, modulaire et modifiable facilement.

import SwiftUI

class MoteurDeJeu:ObservableObject {
  
  @Published var caseCourante:String = "Aucune case"
  
  func toucherCase(_ identifiant:Int) {
    caseCourante = "Case n°" + String(identifiant)
  }
}

struct UneCase: View {
  var moteurJeu:MoteurDeJeu?
  var tag:Int
  var body: some View {
    Rectangle()
      .foregroundColor(Color.blue)
      .frame(width: 100, height: 100)
      .tag(tag)
      .onTapGesture {
        self.moteurJeu?.toucherCase(self.tag)
    }
  }
}

struct ContentView: View {
  
    @ObservedObject var moteurJeu = MoteurDeJeu()
    
    var body: some View {
      VStack {
        Text(moteurJeu.caseCourante)
          .font(.largeTitle)
          .foregroundColor(.blue)
        UneCase(moteurJeu: moteurJeu, tag: 0)
        UneCase(moteurJeu: moteurJeu, tag: 1)
        UneCase(moteurJeu: moteurJeu, tag: 2)
        UneCase(moteurJeu: moteurJeu, tag: 3)
      }
    }
}

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

Un petit détail utile : les Tag des UIViews de UIKit sont limités aux entiers. Alors que les Vues de SwiftUI peuvent utiliser a peu prés n’importe quoi comme Tag. Cette variante de mon exemple utilise des String comme identifiants :

import SwiftUI

class MoteurDeJeu:ObservableObject {
  
  @Published var caseCourante:String = "Aucune case"
  
  func toucherCase(_ identifiant:String) {
    caseCourante = "Tag : " + identifiant
  }
}

struct UneCase: View {
  var moteurJeu:MoteurDeJeu?
  var tag:String
  var body: some View {
    Rectangle()
      .foregroundColor(Color.blue)
      .frame(width: 100, height: 100)
      .tag(tag)
      .onTapGesture {
        self.moteurJeu?.toucherCase(self.tag)
    }
  }
}

struct ContentView: View {
  
    @ObservedObject var moteurJeu = MoteurDeJeu()
    
    var body: some View {
      VStack {
        Text(moteurJeu.caseCourante)
          .font(.largeTitle)
          .foregroundColor(.blue)
        UneCase(moteurJeu: moteurJeu, tag: "TAG_CASE_0")
        UneCase(moteurJeu: moteurJeu, tag: "TAG_CASE_1")
        UneCase(moteurJeu: moteurJeu, tag: "TAG_CASE_2")
        UneCase(moteurJeu: moteurJeu, tag: "TAG_CASE_3")
      }
    }
}

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

Un Tag alphanumérique est plus parlant qu’un vulgaire nombre, ne serait-ce que pour le débugage.

L’idée de base de ForEach est de construire des objets graphiques à partir d’une description statique. Si les propriétés de ces objets peuvent changer, il faut définir leurs états dans un tableau.

J’ai fait une petite démo pour illustrer ça, avec 4 rectangles de couleur bleu. Il suffit d’en toucher un pour passer en rouge et vice-versa.

J’ai commencé par créer une structure pour mémoriser l’état de l’objet (la couleur), bleu par défaut.

struct DescriptionRectangle : Identifiable {
  var id = UUID()
  var couleur = Color.blue
}

ForEach a des exigences particulières pour cette structure. Elle doit être Identifiable, et avoir une variable id contenant un identifiant unique, pour ne pas être confondue avec d’autres données du même type. Heureusement, Swift a une fonction UUID() pour fabriquer un identifiant unique sur demande.

J’ai ensuite écrit un composant graphique ViewRectangle() fabriquant un rectangle à partir de la description.

struct ViewRectangle : View {
  var description:DescriptionRectangle
  var body: some View {

  Rectangle()
    .frame(width: 200, height: 100)
    .foregroundColor(description.couleur)
  }

}

Etape suivante : stocker l’état initial des objets dans une liste.

@State var listeDescriptions = [DescriptionRectangle(),
                                DescriptionRectangle(),
                                DescriptionRectangle(),
                                DescriptionRectangle()]

4 rectangles, donc 4 descriptions. Elles sont identiques puisque tous les rectangles sont bleus.

La boucle ForEach parcoure la liste pour créer les rectangles à partir de leurs description.

var body: some View {
  VStack {
    ForEach(0..<listeDescriptions.count) { index in
      ViewRectangle(description: self.listeDescriptions[index])
        .onTapGesture {
          self.tapRectangle(index: index)
      }
    }
  }
}

Chaque rectangle est associé avec une gesture Tap, liée avec une fonction de la structure View.

Chaque fois que l’utilisateur touche un Rectangle, la fonction tapRectangle() reçoit son numéro d’index (sa position dans le tableau de description).

Pour modifier la couleur d’un Rectangle, il suffit de modifier sa description. Puisque c’est un tableau de type @State, Swift détecte automatiquement le changement et avertit la vue qu’elle doit se redessiner.

La version de test se contente d’inverser la couleur du Rectangle (bleu=>Rouge et inversement).

func tapRectangle(index:Int) {
  if listeDescriptions[index].couleur == Color.blue {
    listeDescriptions[index].couleur = Color.red
  } else {
    listeDescriptions[index].couleur = Color.blue
  }
}

Mais elle pourrais faire des choses plus complexes, comme avertir une classe Swift du contact, et lui demander quoi faire.

J’ai été surpris de voir que l’on peut se passer du tag en donnant directement le numéro d’index dans la closure. C’est pratique. Mais on peut toujours utiliser le .tag si c’est nécessaire (comme pour un tag alphanumérique).

Le code complet :

import SwiftUI

struct DescriptionRectangle : Identifiable {
  var id = UUID()
  var couleur = Color.blue
}

struct ViewRectangle : View {
  var description:DescriptionRectangle
  var body: some View {
    Rectangle()
      .frame(width: 200, height: 100)
      .foregroundColor(description.couleur)
  }
}

struct ContentView: View {
    @State var listeDescriptions = [DescriptionRectangle(),
                                    DescriptionRectangle(),
                                    DescriptionRectangle(),
                                    DescriptionRectangle()]
  
    func tapRectangle(index:Int) {
      if listeDescriptions[index].couleur == Color.blue {
        listeDescriptions[index].couleur = Color.red
      } else {
        listeDescriptions[index].couleur = Color.blue
      }
    }
  
    var body: some View {
      VStack {
        ForEach(0..<listeDescriptions.count) { index in
          ViewRectangle(description: self.listeDescriptions[index])
            .onTapGesture {
              self.tapRectangle(index: index)
          }
        }
      }
  }
}

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

Merci beaucoup pour cette analyse

Voici un début, ou mettre la boucle pour faire jouer l’ordinateur automatiquement une seconde après que le joueur ai joué et basculer le joueur actif.

enum EtatCase {
case vide
case ordi
case joueur
}

enum QuiJoue: String {
case ordiJoue
case joueurJoue
}

struct DescriptionRectangle: Identifiable {
var id = UUID()
var couleur = Color.gray
var etatCase: EtatCase = .vide
}

struct rectangleView: View {
var description: DescriptionRectangle
var body: some View {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(description.couleur)
}
}

struct ContentView: View {
@State var listeDescriptions = [
[DescriptionRectangle(), DescriptionRectangle(),DescriptionRectangle()],
[DescriptionRectangle(), DescriptionRectangle(),DescriptionRectangle()],
[DescriptionRectangle(), DescriptionRectangle(),DescriptionRectangle()]
]

@State var lignes = 3
@State var colonnes = 3
@State var quiJoue: QuiJoue = .ordiJoue

var body: some View {
    VStack {
        ForEach(0..<self.lignes) { ligne in
            HStack {
                ForEach(0..<self.colonnes) { colonne in
                    rectangleView(description: self.listeDescriptions[ligne][colonne])
                        .onTapGesture {
                            self.tapRectangle(ligne: ligne, colonne: colonne)
                    }
                }
            }
        }
        
        HStack {
            Text("\(quiJoue.rawValue) Joue")
                .padding()
            
            
            Button(action: {
                if self.quiJoue == .ordiJoue {
                    self.quiJoue = .joueurJoue
                } else {
                    self.quiJoue = .ordiJoue
                }
            }) {
                Text("Jouer")
            }
            .padding()
        }
    }
}

func tapRectangle(ligne:Int, colonne: Int) {
    if quiJoue == .ordiJoue {
        listeDescriptions[ligne][colonne].couleur = Color.red
    } else {
        listeDescriptions[ligne][colonne].couleur = Color.green
    }
}

}

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

Un peu de théorie : une application, tout particulièrement un jeu vidéo peut être comparée à une machine à état. C’est à dire qu’elle possède plusieurs modes de fonctionnement. Par exemple : initialisation, tour du joueur, tour de l’IA, partie gagnante, partie perdue, terminé, etc …

Chaque état correspond à un fonctionnement bien précis et un affichage spécifique. Par exemple, pendant le tour du joueur le damier doit être désactivé pour éviter que le joueur ne presse dessus. Pendant la phase « partie gagnée » ou « partie perdue », le damier doit aussi être désactivé et un message spécifique (ou une animation) apparait sur l’écran.

On peut gérer ça avec un enum pour les différents états. Et une fonction pour activer un état, genre :

var etatDuJeu : TypeEtat

func activerEtat(etat : TypeEtat) {
  Swift etat :
    case TourDuJoueur
       // activer damier
      // afficher un message
     // mémoriser l'état courant
   case TourIA
      // Désactiver damier
      // Lancer une attente simulant la réflexion
      // Afficher un message
      // mémoriser l'état
   case PartiePerdu
      // Désactiver le damier (ce qui permet d'éviter ton problème de blocage)
      // Afficher un message "Vous avez perdu" ou du même style
  case PartieGagnante
    // Etc ..
}

C’est du pseudo code non opérationnel. Je te répond en vitesse, sans lancer Xcode pour taper un exemple fonctionnel.

Cette technique permet de structurer la pensée, en décomposant le fonctionnement de l’application en blocs fonctionnels bien définis. Mais centralise aussi les phases de transitions, en regroupant à un endroit précis dans le code toutes les actions à faire pour activer/désactiver un état.

Merci pour cette explication très didactique
Cela me rappelle un automate que j’avais fait dans ma vie professionnelle
Merci encore, je vais essayer de la mettre en pratique
Par contre comment insérer cette fonction de le programme actuel

Bravo Michel! Je viens de voir que tu as 6 apps sur l’AppStore, t’es pas là pour tricoter toi :slight_smile:

Eh oui
J’ai l’impression de programmer en mode chasse neige, en programmant un peu à l’ancienne
Un peu de mal a utiliser les classes et les structures et ne pas tout mélanger: code et graphique.
j’apprécie l’aide de draken qui me donne de bons conseils
je suis étonné de toutes les choses graphiques que l’on peut faire avec SwitUI
à bientôt
Michel

Voici une version alternative du système d’affichage, où toute la gestion de la grille de jeu (création et traitement des événements utilisateurs) est traitée dans une classe.

import SwiftUI

struct IndexCase {
  var ligne   = 0
  var colonne = 0
}

struct DescriptionCase : Identifiable {
  var id = UUID()
  var couleur = Color.gray
  var index = IndexCase()
}

struct CaseView : View {
  var description : DescriptionCase
  var body: some View {
    Rectangle()
      .foregroundColor(description.couleur)
      .frame(width: 80, height: 80)
  }
}

class Damier:ObservableObject {
  
  @Published var listeCases :[[DescriptionCase]]
  
  // Action
  func touchCases(index:IndexCase) {
    let ligne = index.ligne
    let colonne = index.colonne
    let description = listeCases[ligne][colonne]
    if description.couleur == Color.gray {
      listeCases[ligne][colonne].couleur = Color.blue
    } else {
      listeCases[ligne][colonne].couleur = Color.gray
    }
  }
  
  private static func creer(nbLignes:Int, nbColonnes:Int) -> [[DescriptionCase]]
  {
    var tableau = [[DescriptionCase]]()
    for ligne in 0..<nbLignes {
      var ligneTableau = [DescriptionCase]()
      for colonne in 0..<nbColonnes {
        var description = DescriptionCase()
        description.index = IndexCase(ligne: ligne, colonne: colonne)
        ligneTableau.append(description)
      }
      tableau.append(ligneTableau)
    }
    return tableau
  }
  
  // Création nouvelle grille vierge
  func nouvelleGrille(nbLignes:Int, nbColonnes:Int) {
    listeCases = Damier.creer(nbLignes: nbLignes, nbColonnes: nbColonnes)
  }
  
  init(nbLignes:Int, nbColonnes:Int) {
    listeCases = Damier.creer(nbLignes: nbLignes, nbColonnes: nbColonnes)
  }
  
}

struct GrilleView : View {
  var liste : [[DescriptionCase]]
  var action: Damier
  var actif:  Bool
  var body: some View {
    ForEach(0..<liste.count, id: \.self) { ligne in
      HStack {
        ForEach(0..<self.liste[ligne].count, id: \.self) { colonne in
          CaseView(description: self.liste[ligne][colonne])
            .onTapGesture {
              if self.actif {
                self.action.touchCases(index: self.liste[ligne][colonne].index)
              }
          }
        }
      }
    }
  }
}

struct ContentView: View {
  @ObservedObject var damier = Damier(nbLignes: 4, nbColonnes: 4)
  @State var grilleActive = true
  
  var body: some View {
    VStack {
      GrilleView(liste : damier.listeCases,
                 action: damier,
                 actif : grilleActive)
      
      Button(action:{
        self.grilleActive.toggle()
      } ) {
        Text("Activation/Désactivation")
          .font(.title)
          .bold()
      }
      .padding(.vertical, 40)
      
      Button(action: {
        self.damier.nouvelleGrille(
            nbLignes: Int.random(in: 2...4),
            nbColonnes: Int.random(in: 2...4))
      }) {
        Text("Nouvelle Grille")
          .font(.title)
          .bold()
      }
    }
  }
}

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

Le composant GrilleView qui s’occupe de l’affichage de la grille a besoin de 3 paramètres :

  • la liste des cases à dessiner
  • l’objet devant recevoir les événements utilisateurs (quand le joueur sélectionne une case)
  • une valeur booléenne pour activer ou désactiver la gestion des événements utilisateurs (pour éviter que la sélection inopportune d’une case ne lance un traitement, alors que c’est le tour de l’IA de jouer, ou la fin de la partie).

L’exemple permet de tester le bon fonctionnement du code.

grillage

Les fonctions de la classe Damier permettent de créer des grilles de différentes tailles, et de réagir aux actions du joueur.

La méthode touchCases() gére ce qui se passe quand le joueur sélectionne une case. Dans cette version de test, elle inverse juste la couleur de la case (gris devient bleu et inversement).

func touchCases(index:IndexCase) {
        let ligne = index.ligne
        let colonne = index.colonne
        let description = listeCases[ligne][colonne]
        if description.couleur == Color.gray {
          listeCases[ligne][colonne].couleur = Color.blue
        } else {
          listeCases[ligne][colonne].couleur = Color.gray
        }
      }

Un grand merci
Tout dans une classe c’est la classe!

j’ai essayé de sortir la structure grilleView et caseView de la classe Damier de façon a avoir
toutes les vues à part et cela fonctionne !

struct GrilleView: View {
var liste: [[DescriptionCase]]
var action: Damier
var actif: Bool

var body: some View {
    VStack {
        ForEach(0..<liste.count, id: \.self) { ligne in
            HStack {
                ForEach(0..<self.liste[ligne].count, id:\.self) { colonne in
                    CaseView(description: self.liste[ligne][colonne])
                        .onTapGesture {
                            if self.actif {
                                self.action.toucheCases(index: self.liste[ligne][colonne].index)
                            }
                    }
                }
            }
        }
    }
}

}

My name is Blue, Blue Dragon !

Bonjour Blue Dragon

je n’ai pas trouvé comment désactiver le clavier pendant que l’ordinateur joue
j’ai fait un test de fin de partie ou je désactive le clavier

Dans damier j’ai ajouté

func ordiCase() {
while true {
let ligne = Int.random(in: 0…<3)
let colonne = Int.random(in: 0…<3)

        if listeCases[ligne][colonne].couleur == Color.gray {
            listeCases[ligne][colonne].couleur = Color.red
            break
        }
    }
}

et GrilleView

struct GrilleView: View {
var liste: [[DescriptionCase]]
var action: Damier
@Binding var actif: Bool
@State private var nbCasesJouees = 0

var body: some View {
    VStack {
        ForEach(0..<liste.count, id: \.self) { ligne in
            HStack {
                ForEach(0..<self.liste[ligne].count, id:\.self) { colonne in
                    CaseView(description: self.liste[ligne][colonne])
                        .onTapGesture {
                            if self.actif {
                                // Joueur joue
                                self.action.toucheCases(index: self.liste[ligne][colonne].index)
                                
                                // Ordi joue
                                self.action.ordiCase()
                                self.nbCasesJouees += 1
                                
                                // Fin de Jeux
                                if self.nbCasesJouees == 4 {
                                    self.nbCasesJouees = 0
                                    self.actif.toggle()
                                }
                                
                            } else {

// self.action.ordiCase()

                            }
                    }
                }
            }
        }
    }
}

}

Tu aimes vraiment le code spaghettis et les trucs partant tous les sens, non ? Le principe de la programmation objet c’est de créer de petits modules s’occupant de tâches élémentaires. Et de les assembler ensuite, comme des briques de Logo dans une architecture globale.

GrilleView c’est juste pour afficher des objets sur l’écran, et reporter les événements utilisateurs dans un autre module. C’est ce que j’avais fait dans mon exemple, en créant une fonction touchCases() dans la classe Damier.

Je n’ai pas le temps là, de compléter mon exemple pour te montrer comment faire. Je m’y colle ce soir.

c’est pour çà que j’ai créé ordiCases() provisoirement qui pourrait être inclus dans toucheCases() mais c’est le passage du joueur à l’ordinateur qui me pose problème

Voici un exemple d’une machine a état :

machine_etat

L’interface de jeu est composé de deux compteurs indépendants, affichant le nombre de fois que l’utilisateur a cliqué dessus. Cela simule le damier de jeu.

Si le joueur presse le bouton Activer Mode IA, il se passe des tas de choses :

  • Les compteurs ne fonctionnent plus
  • Le fond d’écran passe en gris
  • Le bouton disparaît

Au bout de 5 secondes, l’écran redevient blanc, les compteurs sont de nouveau actifs et le bouton est revenu.

L’exemple peut se trouver dans 5 états différents :

enum EtatJeu : String {
  case indetermine = "Indéterminé"
  case tourJoueur = "Tour Joueur"
  case tourIA = "Tour IA"
  case joueurGagnant = "Joueur Gagnant"
  case joueurPerdant = "Joueur Perdant"
}

Le premier état est indéterminé. Il n’existe qu’au lancement de l’application, quand les différentes initialisations ne sont pas encore terminées.

Chaque état active ou désactive certains paramètres des objets du jeu. La fonction activerEtat() regroupe toutes ces activations/désactivations au même endroit.

  func activerEtat(etat:EtatJeu) {
        switch etat {
        case .indetermine:
          break
        case .tourJoueur:
          interfaceActive = true
          couleurFond = Color.white
          boutonIAvisible = true
        case .tourIA:
          couleurFond = Color.gray
          interfaceActive = false
          boutonIAvisible = false
          DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            self.activerEtat(etat: .tourJoueur)
          }
        case .joueurGagnant:
          interfaceActive = false
          boutonIAvisible = false
          // Faire d'autres choses ..
        case .joueurPerdant:
          interfaceActive = false
          boutonIAvisible = false
          // Faire d'autres choses ..
        }
        etatJeu = etat
      }
    }

L’état .tourJoueur est activé automatiquement lorsque la vue s’affiche sur l’écran, la première fois, grâce à la propriété OnAppear.

 .onAppear(
        perform: { self.activerEtat(etat: .tourJoueur)})

Le passage en mode IA avec le bouton, se fait tout aussi facilement :

Button(action: { self.activerEtat(etat: .tourIA)}) {
  Text("Activer Mode IA")
    .font(.largeTitle)
    .bold()
    .opacity(boutonIAvisible ? 1 : 0)
}

Pour passer en mode IA, la fonction doit :

  • changer la couleur de fond en gris pour bien visualiser le nouvel état
  • désactiver l’interface pour que les compteurs ne fonctionnent plus
  • faire disparaître le bouton d’activation
  • demander à iOS de repasser en mode .tourJoueur 5 secondes plus tard

Le délai est de 5 secondes pour que l’utilisateur ai le temps de cliquer sur les compteurs, et de constater qu’ils ne fonctionnent pas dans le modeIA.

case .tourIA:
  couleurFond = Color.gray
  interfaceActive = false
  boutonIAvisible = false
  DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    self.activerEtat(etat: .tourJoueur)
  }

C’est bien mieux qu’un code spaghettis avec des variables modifiées ici et là.

Le code complet :

import SwiftUI

struct Compteur:View {
  @Binding var compteur:Int
  @Binding var composantActif : Bool
  var couleur : Color
  var body : some View {
    Text(String(compteur))
      .font(.largeTitle)
      .bold()
      .foregroundColor(Color.white)
      .frame(width: 100, height: 100)
      .background(couleur)
      .onTapGesture {
        if self.composantActif { self.compteur += 1 }
    }
  }
}

enum EtatJeu : String {
  case indetermine    = "Indéterminé"
  case tourJoueur     = "Tour Joueur"
  case tourIA         = "Tour IA"
  case joueurGagnant  = "Joueur Gagnant"
  case joueurPerdant  = "Joueur Perdant"
}

struct ContentView: View {
    @State var etatJeu = EtatJeu.indetermine
    @State var interfaceActive = true
    @State var compteur1 = 0
    @State var compteur2 = 0
    @State var couleurFond = Color.white
    @State var boutonIAvisible = true
  
    var body: some View {
      VStack {
        Spacer()
        Text(etatJeu.rawValue)
          .font(.system(size: 50)).bold()
        Spacer()
        HStack {
          Spacer()
          Compteur(compteur: $compteur1,
                   composantActif: $interfaceActive,
                   couleur: Color.green)
          Spacer()
          Compteur(compteur: $compteur2,
                   composantActif: $interfaceActive,
                   couleur: Color.blue)
          Spacer()
        }
        Spacer()
        
        Button(action: { self.activerEtat(etat: .tourIA)}) {
          Text("Activer Mode IA")
            .font(.largeTitle)
            .bold()
            .opacity(boutonIAvisible ? 1 : 0)
        }
        Spacer()
        
      } .onAppear(
        perform: { self.activerEtat(etat: .tourJoueur)})
      .background(couleurFond)
  }
  
  func activerEtat(etat:EtatJeu) {
    switch etat {
    case .indetermine:
      break
    case .tourJoueur:
      interfaceActive = true
      couleurFond = Color.white
      boutonIAvisible = true
    case .tourIA:
      couleurFond = Color.gray
      interfaceActive = false
      boutonIAvisible = false
      DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
        self.activerEtat(etat: .tourJoueur)
      }
    case .joueurGagnant:
      interfaceActive = false
      boutonIAvisible = false
      // Faire d'autres choses ..
    case .joueurPerdant:
      interfaceActive = false
      boutonIAvisible = false
      // Faire d'autres choses ..
    }
    etatJeu = etat
  }
}

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

J’espère que cela répond à ta question sur la manière de faire passer le jeu d’un état à un autre.