Tutoriels & Doc
Pong (C++ 2/5)
Intelligence artificielle

Intelligence artificielle

Étape 5
Étape terminée ?

Jouer contre les humains c'est cool, mais créer une IA c'est mieux.

Dans cette étape, nous allons:

  • Ajouter une intelligence artificielle simple

  • Ajouter de l'aléatoire dans le jeu

Vous avez fini votre premier jeu, améliorons-le avec de l'intelligence artificielle.

Notre première IA

Durée 30 minutes (et plus si affinité)

Niveau débutant

Prérequis

Dans l'étape précédente: Pong, vous avez créé un jeu à deux joueurs fonctionnel. On y utilisait les flèches pour bouger la raquette de gauche, et les boutons A et B pour la raquette de droite. Avec ce qu'on a vu à l'étape compteur d'invités, vous avez aussi implémenté un système de points. Enfin, nous avons utilisé des if avec plusieurs conditions pour faire rebondir la balle.

Pong à deux c'est sympa, mais comment faire si on veut jouer tout seul? Figurez-vous qu'une petite intelligence artificielle vous permettra de jouer seul contre votre Gamebuino ;)

Mais, qu'est-ce qu'une intelligence artificielle au juste? L'intelligence artificielle, aussi appelée ** IA** consiste à donner le pouvoir à l'ordinateur de prendre des décisions par lui-même: C'est le rendre vivant!

Une IA peut être très puissante. C'est un domaine très populaire aujourd'hui, et il peut prendre des formes assez complexes. Il y a même des diplômes universitaires spécialisés dans l'IA. Dans cet atelier, nous allons revenir aux bases de l'IA, aux origines. On va créer une intelligence pour une des raquettes de notre jeu Pong.

La manière la plus simple de programmer une IA est d'utiliser des déclarations conditionnelles if plus poussées. Avec de l'aléatoire, nous rendrons l'ordinateur moins prévisible et plus amusant dans la partie 2 de cette étape.

Notre premier ennemi virtuel

Commençons par examiner comment l'ordinateur pourra atteindre ses objectifs. L'ordinateur contrôlera la raquette de droite, appelée raquette2 dans le pong de l'atelier précédent. L'objectif de cette IA est de taper la balle quand elle arrive de son côté. Autrement dit, il faut que l'ordinateur puisse suivre la balle avec la raquette.

La logique derrière les coulisses

L'algorithme devrait donc avoir cette forme:

Si la balle est au-dessus du centre de la raquette
    Alors, déplacer la raquette vers le haut
Sinon, si la raquette est en dessous du centre de la raquette
    Alors, déplacer la raquette vers le bas

Avec cet algorithme, on veut toujours faire une de ces deux choses:

  • aller vers le haut,
  • ou aller vers le bas.

De manière généralle, on veut mettre la raquette face à la balle. Pour faire ça, on regarde si la balle est au-dessus ou en dessous du centre de la raquette. Vous avez vu que la variable raquette2_posY contient la position du haut de la raquette. Si on veut le centre, il nous faut y ajouter la moitié de la hauteur de la raquette. On a donc raquette2_centreY = raquette2_posY + raquette_hauteur / 2.

L'algorithme introduit aussi quelque chose de nouveau pour nous: le sinon. Il arrive que vous vouliez exprimer l'idée de "Si A est vrai, alors on fait une chose, sinon on fait une autre chose." En C/C++, ça se traduit par:

if (A) {
  // Faire une chose
}
else {
  // Faire une autre chose
}

Ceux d'entre vous qui ne parlent pas anglais auront peut-être deviné que "else" signifie "sinon" en français ;) Une autre particularité très pratique est le else if. Quand notre sinon est directement suivi par un if, alors on peut écrire else if () {...} à la place de else { if () {...} }. C'est plus lisible et surtout on peut placer autant de else if à la suite que l'on souhaite (alors qu'on peut ne mettre qu'un seul else). Par exemple, ceci est permis:

if (A) {
  // Faire une chose
}
else if (B) {
  // Faire une autre chose
}
else if (C) {
  // Faite encore autre chose
}
else {  // Si A, B, et C sont faux
  // Faire autre chose
} 

Pour en revenir à notre algorithme, on a donc un if et un else if. En appliquant ce qu'on vient de voir, il suffit donc de trouver le code correspondant à l'algorithme. Maintenant c'est...

Notre IA en code

Vous avez tous les outils à votre disposition pour pouvoir programmer votre première intelligence artificielle. Commencez par relire l'algorithme, puis modifiez le code de l'étape précédente (Pong - deux joueurs) à l'aide de ce qu'on vient de voir. Si vous avez une idée mais que vous n'êtes pas sûr, essayez-la quand même! Vous avez le code, vous avez la console, quelle meilleure façon de savoir si ça marche que de l'essayer?

Petit coup de main: pour trouver le centre de la raquette, relisez le début du tutoriel.

J'ai mis la solution juste en dessous, donc si vous n'avez pas cherché par vous-même, c'est votre dernière chance :) Encore une fois, essayer par soi-même est une des meilleurs techniques pour apprendre rapidement.

Et voilà, c'est fait! Vous pouvez jouer contre votre Gamebuino à Pong! Mais... en jouant vous avez peut-être remarqué que la raquette ennemie suit le mouvement exact de la balle et fait peu de fautes. Nous allons corriger ça en améliorant notre IA.

Exemple de Solution

Ok, donc regardons si vous avez quelque chose de similaire:

void loop() {
  //// MAJ de la raquette1 (raquette du joueur)

  // MAJ de la raquette2 (raquette de l'ordinateur)
  if (balle_posY > raquette2_posY + raquette_hauteur / 2) {  // Si la balle est plus basse que le centre
    raquette2_posY = raquette2_posY + 1;                     // Se déplacer vers le bas
  } 
  else if (balle_posY < raquette2_posY + raquette_hauteur / 2) {  // Si la balle est plus haute que le centre
    raquette2_posY = raquette2_posY - 1;                          // Se déplacer vers le haut
  }

  //// MAJ de la balle et collisions
  //// Afficher la balle, les raquettes et le score 
}

Une IA moins prévisible

Vous avez conçu une intelligence artificielle. L'ordinateur est capable de suivre la balle pour ne pas perdre. Il joue contre vous mais son comportement est très prévisible. 'Si la balle est en dessous de la raquette, je descends. Sinon je monte'. Ce comportement n'est pas très naturel. Ça serait mieux si notre adversaire se déplaçait plus chaotiquement et ratait la balle de temps en temps.

Random()

Mais comment concevoir un comportement imprévisible? Et bien il existe une fonction qui nous permet de générer un nombre aléatoire:

random(int min, int max) random() est une fonction qui retourne un nombre entier aléatoire. Le nombre renvoyé sera entre min et max - 1. C'est-à-dire que si a = random(0, 4);, alors a peut valoir 0, 1, 2, ou 3 (mais pas 4!). Avant d'attaquer notre IA avancée, utilisons random() pour améliorer notre jeu.

Quand vous avez créé votre jeu Pong, la balle est replacée sur l'écran à chaque fois qu'un joueur marque un point. Voici le code qui vous permettait de le faire:

// Vérifier si la balle est sortie de l'écran
if (balle_posX < 0) {
  // Replacer la balle sur l'écran
  balle_posX = 20;
  balle_posY = 20;
  balle_speedX = 1;
  balle_speedY = 1;
  // incrémenter le score du joueur 2
  score2 = score2 + 1;
}
if (balle_posX > gb.display.width()) {
  // Replacer la balle sur l'écran
  balle_posX = 20;
  balle_posY = 20;
  balle_speedX = 1;
  balle_speedY = 1;
  // incrémenter le score du joueur 1
  score1 = score1 + 1;
}

En faisant comme ça, toutes les parties commencent de la même manière: la balle apparait en position (20, 20) et part vers le bas à droite. C'est un peu répétitif non? random() peut nous sauver!

// Vérifier si la balle est sortie de l'écran
if (balle_posX < 0) {
  // Replacer la balle sur l'écran
  balle_posX = 20;
  balle_posY = random(20, gb.display.height() - 20);  // Position aléatoire au centre de l'écran
  balle_speedX = 1;

  if (random(0, 2) == 1) {  // 50% du temps
    balle_speedY = 1;
  } 
  else {  // 50% du temps
    balle_speedY = -1;
  }

  // incrémenter le score du joueur 2
  score2 = score2 + 1;
}

if (balle_posX > gb.display.width()) {
  // Replacer la balle sur l'écran
  balle_posX = 20;
  balle_posY = random(20, gb.display.height() - 20);  // Position aléatoire au centre de l'écran
  balle_speedX = 1;

  if (random(0, 2) == 1) {  // 50% du temps
    balle_speedY = 1;
  } 
  else {  // 50% du temps
    balle_speedY = -1;
  }

  // incrémenter le score du joueur 1
  score1 = score1 + 1;
}

Maintenant, la balle réapparait en (20, Y), avec Y un nombre aléatoire. De plus, comme leif (random(0, 2) == 1) est vrai une fois sur deux car random(0, 2) retourne soit 0, soit 1, la balle part dans une direction aléatoire: en haut ou en bas. Ce sont des petites modifications comme ça qui différencient les jeux moyens des bons jeux ;)

Remarque: En C/C++, si on veut tester une égalité dans une condition, il faut utiliser un double égal ==. Donc if (a == b) est vrai quand a vaut b. C'est TRÈS important car si vous mettez un seul signe égal, votre jeu fera des choses inattendues.

Moins prévisible == plus amusant

Améliorons notre IA avec random()! Pour que l'ordinateur semble plus "humain", il lui faut des réflexes plus longs. Pour l'instant, la raquette est toujours en face de la balle, mais avec random, on peut faire en sorte que la raquette ait plus de mal à suivre. Pour compenser un peu ses erreurs, on va aussi augmenter sa vitesse de déplacement. Avec tout ça on aura un ennemi bien plus dynamique et amusant.

Tout d'abord, pour que la raquette ait un mouvement fluide, il faut créer une variable de vitesse comme on a fait pour la balle: raquette2_speedY. Regardons son utilité:

int raquette2_speedY = 0;  // Vitesse verticale de la raquette2

void loop() {
  //// MAJ de la raquette1 (raquette du joueur)

  // MAJ de la raquette2 (raquette de l'ordinateur)
  if (balle_posY > raquette2_posY + raquette_hauteur / 2 && random(0, 3) == 1) {
    raquette2_speedY = 2;  // Vers le bas
  } else if (balle_posY < raquette2_posY + raquette_hauteur / 2 && random(0, 3) == 1) {
    raquette2_speedY = -2;  // Vers le haut
  }

  raquette2_posY = raquette2_posY + raquette2_speedY;  // Mettre à jour la position de la raquette2

  //// MAJ de la balle et collisions
  //// Afficher la balle, les raquettes et le score 
}

Ici, nous avons ajouté la condition random(0, 3) == 1. Donc le premier if est vrai une fois sur 3 quand la balle est plus basse que la raquette (de même quand la balle est au-dessus de la raquette). Pour mieux comprendre, imaginez le scénario suivant: la balle est sous la raquette, la raquette se dirige vers le bas (raquette2_speedY = 2). Quand la raquette dépasse la balle, sa direction devrait changer, mais avec le random() peut-être que la raquette continuera de descendre. Avec un peu de chance, elle ira trop loin et vous aurez gagné!

Dans le code que je vous donne, on a random(0, A) == 1 avec A = 3. Logiquement, plus A est grand plus la condition devient improbable. Donc la valeur de A détermine la vitesse de réaction de notre IA car il lui faudra plus d'essais avant de changer de direction. On peut exploiter ce fait pour faire varier la difficulté, mais pour ça, je vous laisse faire.

A vous de jouer !

Dans cet atelier, nous avons créé notre IA puis nous l'avons améliorée avec random(). Comme exercice de fin d'atelier, je vous propose d'implémenter une nouvelle fonctionnalité: changer de difficulté. En appuyant sur le bouton MENU, le joueur peut changer entre "facile" et "difficile".

Astuce : Pour faire varier la difficulté, relisez la fin de cet atelier (juste avant le "Encore à vous de jouer").

A vous de continuer de perfectionner votre IA, ou de vous en inspirer pour faire une IA dans un autre jeu ;)

Montrez votre talent sur les réseaux sociaux avec #gamebuino #Pong #IA , on vous suit de près ;)

Exemple de solution

Si vous êtes en panne d'inspiration, voilà ce qu'on a fait de notre côté :)

#include <Gamebuino-Meta.h>

 




// Caractéristiques de la balle
int balle_posX = 20;
int balle_posY = 20;
int balle_speedX = 1;
int balle_speedY = 1;
int balle_taille = 3;




// Caractéristiques des raquettes
int raquette1_posX = 10;
int raquette1_posY = 30;
int raquette2_posX = gb.display.width() - 13;
int raquette2_posY = 30;
int raquette_hauteur = 10;
int raquette_largeur = 3;




// Pour l'IA
int raquette2_speedY = 0;  // Vitesse verticale de la raquette2




// Scores
int score1;  // Score du joueur 1
int score2;  // Score du joueur 2




int difficulte = 3;  // Niveau de difficulté. 3 = FACILE et 2 = DIFFICILE




void setup() {
  gb.begin();
}




void loop() {
  while (!gb.update());
  gb.display.clear();




  // Changement de difficulté
  if (gb.buttons.pressed(BUTTON_MENU)) {
    if (difficulte == 3) { // Facile
      difficulte = 2;  // Changer de difficulté
    }
    else {  // Difficile
      difficulte = 3;  // Changer de difficulté
    }
  }




  // MAJ raquette1
  if (gb.buttons.repeat(BUTTON_UP, 0)) {
    raquette1_posY = raquette1_posY - 1;
  }
  if (gb.buttons.repeat(BUTTON_DOWN, 0)) {
    raquette1_posY = raquette1_posY + 1;
  }




  // MAJ Raquette2 - Intelligence Artificielle
  if (balle_posY > raquette2_posY + raquette_hauteur / 2 && random(0, difficulte) == 1) {
    raquette2_speedY = 2;
  } else if (balle_posY < raquette2_posY + raquette_hauteur / 2 && random(0, difficulte) == 1) {
    raquette2_speedY = -2;
  }
  raquette2_posY = raquette2_posY + raquette2_speedY;  // Mettre à jour la position de la raquette2




  // MAJ balle
  balle_posX = balle_posX + balle_speedX;
  balle_posY = balle_posY + balle_speedY;




  // Collisions avec les murs (haut et bas)
  if (balle_posY < 0) {
    balle_speedY = 1;
  }
  if (balle_posY > gb.display.height() - balle_taille) {
    balle_speedY = -1;
  }




  // Collision balle/raquette1
  if ( (balle_posX == raquette1_posX + raquette_largeur)
       && (balle_posY + balle_taille >= raquette1_posY)
       && (balle_posY <= raquette1_posY + raquette_hauteur) ) {
    balle_speedX = 1;
  }
  // Collision balle/raquette2
  if ( (balle_posX + balle_taille == raquette2_posX)
       && (balle_posY + balle_taille >= raquette2_posY)
       && (balle_posY <= raquette2_posY + raquette_hauteur) ) {
    balle_speedX = -1;
  }




  // Vérifier si la balle est sortie de l'écran
  if (balle_posX < 0) {
    // Replacer la balle sur l'écran
    balle_posX = 20;
    balle_posY = random(20, gb.display.height() - 20);  // Position aléatoire au centre de l'écran
    balle_speedX = 1;
    if (random(0, 2) == 1) {  // 50% du temps
      balle_speedY = 1;
    } 
    else {  // 50% du temps
      balle_speedY = -1;
    }




    // incrémenter le score du joueur 2
    score2 = score2 + 1;
  }
  if (balle_posX > gb.display.width()) {
    // Replacer la balle sur l'écran
    balle_posX = 20;
    balle_posY = random(20, gb.display.height() - 20);  // Position aléatoire au centre de l'écran
    balle_speedX = 1;
    if (random(0, 2) == 1) {  // 50% du temps
      balle_speedY = 1;
    } 
    else {  // 50% du temps
      balle_speedY = -1;
    }




    // incrémenter le score du joueur 1
    score1 = score1 + 1;
  }




  // Afficher la balle
  gb.display.fillRect(balle_posX, balle_posY, balle_taille, balle_taille);
  // Afficher la raquette1
  gb.display.fillRect(raquette1_posX, raquette1_posY, raquette_largeur, raquette_hauteur);
  // Afficher la raquette2
  gb.display.fillRect(raquette2_posX, raquette2_posY, raquette_largeur, raquette_hauteur);




  // Afficher les scores
  gb.display.setCursor(35, 5);
  gb.display.print(score1);
  gb.display.setCursor(42, 5);
  gb.display.print(score2);




  // Afficher la difficulté
  gb.display.setCursor(33, gb.display.height() - 5);
  if (difficulte == 3) {
    gb.display.print("EASY");
  }
  else {
    gb.display.print("HARD");
  }
}

Étapes