# Artificial Intelligence (Pong)

By Aurélien Rodot, 7 months ago

Incomplete workshop Workshop completed

So you just finished your first game, let's improve on it with Artificial Intelligence.

## Our first AI

Length 30 minutes

Level Beginner

Prerequisites

In the previous workshop, Pong, you built a functional two-player game. We used the arrows to move the left paddle, and the A and B buttons to move the right paddle. With what we saw in the tally counter workshop, you also implemented a way to track and display the scores. Finally, you used if statements with multiple conditions to make the ball bounce. Well, a 2-player tennis game is nice, but what if we wanted to play alone? Turns out, a small Artificial Intelligence can go a long way ;)

But, what is an artificial intelligence? Artificial Intelligence, also called AI, boils down to giving the computer the power to decide things by itself. It is just like bringing the computer to life!

An AI can be very powerful. It has been hot topic for quite some time now, and it can come in many shapes and forms, including some very complex ones. There are even university degrees specialized in AI. But for this workshop, we will go back to the roots of AI. Our AI will control one of the paddles in our Pong game.

First we will see the simplest way to program an AI, by using if statements. Then, in a second part, with the help of randomness, we will make the computer less predictable and way more fun.

## Our first computer controlled enemy

Let's start by taking a look at how the computer might play. The computer will control the right paddle (called `paddle2` in the previous workshop). Our AI's goal is to prevent the ball to reach the right side of the screen. In other words, the computer must follow the ball with its paddle.

### The logic behind the scenes

Such an algorithm would look like this:

```If the ball is above the center of the paddle
Else, if the ball is below the center of the paddle

Here, we always want to do one of two things: go up or go down. Generally speaking, we want to center the paddle on the ball. To do so, we check if the ball is below the center of the paddle. Since the variable `paddle2_posY` keeps track of the top of the paddle, then we have to add half the paddle's height to get it's center's Y position. So this give us: `paddle2_center = paddle2_posY + paddle_height / 2`.

The algorithm introduces a new concept for us: the else. Sometimes you might want to express "If A is true, then do thing 1, else do thing 2". In C/C++, this is translated by :

```if (A) {
// Do thing 1
}
else {
// Do thing 2
}```

Another awesome aspect of C++ is the `else if {...}`. Whenever an else is directly followed by an if, then we can write `else if () {...}` instead of `else { if () {...} }`. This improves readability quite a bit, but more importantly, we can put as many else ifs as we want in a row (whereas we can only put at most one else per if statement. For example, this is allowed:

```if (A) {
// Do a thing
}
else if (B) {
// Do another thing
}
else if (C) {
// Do something else
}
else {  // A, B, and C are false
// Do yet again another thing
} ```

So, to go back to our algorithm, we have an if and an else if. By simply using what we have just learned, it should be easy enough to translate the algorithm. So now...

### Coding the AI

You now have all the necessary tools to start making you first artificial intelligence. Start by going over the algorithm, then modify the previous' workshop code (Pong - two players) to match the algorithm. If you have an idea but you are not sure about it, try it out! You have the code, you have the console, it is easy to test something. There is no better way to know if it works than by testing it :)

Small help: to find the center of the paddle, go back to the beginning of this tutorial.

I have put the solution for this right below, so if you still have not searched on you own, this is you last chance ;) Once again, the best way to progress is to do (and not just read about) the thing you want to learn!

Ok, so it is done, your own artificial intelligence! You can now play against your Gamebuino at Pong. But... as you play, you might heve noticed that the opposing paddle follows the exact movement of the ball and rarely makes any mistakes. Let's fix that right away.

Solution

This code is our implementation of the AI, you should have something similar:

```void loop() {

if (ball_posY > paddle2_posY + paddle_height / 2) {  // If the ball is below the center of the paddle
}
else if (ball_posY < paddle2_posY + paddle_height / 2) {  // If the ball is above the center of the paddle
}

//// Update ball movement and collisions
//// Draw the ball, the paddles, and the score
}```

## Making our AI less predictable

You have just built a working AI. The computer is capable of following the ball and not losing. It plays correctly, however its behavior is very predictable. 'If the ball is below me, I go down. If the ball is above me, I go up'. This behavior in not very natural looking. The game would be a lot more fun if the AI moved in a more "human" way, and missed the ball from time to time.

### Random()

So how do we go about making an unpredictable movement? Well, there is a legend about a function that returns random numbers! And guess what? It's called `random()`:

`random(int min, int max)`

random() is a function that spits out a random integer. This integer is always between min and max-1. So, the instruction `a = random(0, 4);` means that `a` could be 0, 1, 2, or 3 (but not 4!). Before starting to work on our advanced AI, let's put `random()` to the test.

When you built your Pong game, the ball always spawned in the same position whenever a point was scored. Here is the code we wrote:

```// Check if the ball exited the screen
if (ball_posX < 0) {
// Reset ball
ball_posX = 20;
ball_posY = 20;
ball_speedX = 1;
ball_speedY = 1;
// Increment player 2's score
score2 = score2 + 1;
}
if (ball_posX > gb.display.width()) {
// Reset ball
ball_posX = 20;
ball_posY = 20;
ball_speedX = 1;
ball_speedY = 1;
// Increment player 1's score
score1 = score1 + 1;
}```

By doing so, ALL games started out the same way: the ball spawns in (20, 20) and moved down and to the right. This is a bit repetitive no? `random()` can save us!

```// Check if the ball exited the screen
if (ball_posX < 0) {
// Reset the ball
ball_posX = 20;
ball_posY = random(20, gb.display.height() - 20);  // Random position along the Y axis
ball_speedX = 1;

if (random(0, 2) == 1) {  // 50% of the time, this is true
ball_speedY = 1;
}
else {  // Other 50% of the time
ball_speedY = -1;
}

// Increment player 2's score
score2 = score2 + 1;
}

if (ball_posX > gb.display.width()) {
// Reset ball
ball_posX = 20;
ball_posY = random(20, gb.display.height() - 20);  // Random position along the Y axis
ball_speedX = 1;

if (random(0, 2) == 1) {  // 50% of the time, this is true
ball_speedY = 1;
}
else {  // Other 50% of the time
ball_speedY = -1;
}

// Increment player 1's score
score1 = score1 + 1;
}```

So now, the ball will respawn at (20, Y), where Y is a random number. Also, we use `if (random(0, 2) == 1)` because it is true only every other time. You see, `random(0, 2)` returns either 0 or 1, so there is a 50% chance of getting a 1. With this, we set the ball to start with an upward motion half of the time, and a downward motion the rest of the time. It is these slight modifications that differentiate a 'meh' game from a great game ;)

In C/C++, if we want to test an equality in a condition, we have to use a double equals sign `==`. So `if (a == b)` is true when `a` IS EQUAL TO `b`. This is very important to keep in mind, because if you put a single equals sign, your game will do strange things.

### Less predictable == more enjoyable

Let's improve our AI. To make the computer behave more naturally, it needs to not be perfect at following the ball. With random, we can make it so that the enemy paddle has a harder time following the ball. To help the AI out a bit, we will also increase it's speed. With these changes, the game should a lot more fun to play.

Let's start by making a more fluid movement. To do so, we need to create a variable that keeps track of the computer's paddle's speed, just like we did for the ball: `paddle2_speedY`. Let's look at what we will do with it:

```int paddle2_speedY = 0;  // Vertical speed of the AI's paddle

void loop() {

if (ball_posY > paddle2_posY + paddle_height / 2 && random(0, 3) == 1) {
paddle2_speedY = 2;  // Move down
} else if (ball_posY < paddle2_posY + paddle_height / 2 && random(0, 3) == 1) {
paddle2_speedY = -2;  // Move up
}

//// Update ball movement and collisions
//// Draw the ball, the paddles, and the score
}```

Here, we added the `random(0, 3) == 1` condition. So when the ball is below the paddle's center, the first if is true about once every 3 frames (and it the same is true for the second if). To better understand why this works, imagine the following scenario: the ball is below the paddle, and the paddle is already moving downwards (paddle2_speedY = 2). When the ball ends up above the paddle's center, the paddle's direction should change, but with the random condition, it should take a few frames before the paddle actually decides to go up. And with a little bit of luck, the paddle will miss the ball, and you score a point!

In the code above, we can generalize `random(0, 3) == 1` with `random(0, A) == 1` where A = 3. And the bigger A gets, the less likely the condition becomes. So in this case, A is related to the AI's reaction speed because if A is bigger, the more frames are necessary for the AI to change directions. We can exploit this finding to change the AI's difficulty, but I will leave this up to you :)

In this workshop, we created our first AI, then we improved upon it with the random() function. Like at the end of every workshop, I propose a feasible exercise to implement/improve a functionality. Here I want you to make it so the AI can change difficulty with the press of a button. When the player presses the MENU button, the AI switches between "easy" and "hard" play styles.

Tip: To make the difficulty vary, go over the last part of this tutorial.

It's up to you to polish your AI, or to make another one for another game ;)

Show off your talent on social networks with #gamebuino #Pong #AI, we go through them all the time ;)

## Solution Example

If you ran out of ideas, here is what we did on our side :)

```#include

// ball attributes
int ball_posX = 20;
int ball_posY = 20;
int ball_speedX = 1;
int ball_speedY = 1;
int ball_size = 3;

int paddle2_posX = gb.display.width() - 13;

// For the AI

// Scores
int score1;  // Player 1's score
int score2;  // Player 2's score

int difficulty = 3;  // Level of difficulty. 3 = EASY et 2 = HARD

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

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

// Difficulty switch
if (difficulty == 3) { // Easy
difficulty = 2;  // Change difficulty
}
else {  // Hard
difficulty = 3;  // Change difficulty
}
}

if (gb.buttons.repeat(BUTTON_UP, 0)) {
}
if (gb.buttons.repeat(BUTTON_DOWN, 0)) {
}

if (ball_posY > paddle2_posY + paddle_height / 2 && random(0, difficulty) == 1) {
} else if (ball_posY < paddle2_posY + paddle_height / 2 && random(0, difficulty) == 1) {
}

// Update ball
ball_posX = ball_posX + ball_speedX;
ball_posY = ball_posY + ball_speedY;

// Collisions with walls
if (ball_posY < 0) {
ball_speedY = 1;
}
if (ball_posY > gb.display.height() - ball_size) {
ball_speedY = -1;
}

&& (ball_posY + ball_size >= paddle1_posY)
ball_speedX = 1;
}
if ( (ball_posX + ball_size == paddle2_posX)
&& (ball_posY + ball_size >= paddle2_posY)
ball_speedX = -1;
}

// Check if the ball exited the screen
if (ball_posX < 0) {
// Reset the ball
ball_posX = 20;
ball_posY = random(20, gb.display.height() - 20);  // Random position along the Y axis
ball_speedX = 1;

if (random(0, 2) == 1) {  // 50% of the time, this is true
ball_speedY = 1;
}
else {  // Other 50% of the time
ball_speedY = -1;
}

// Increment player 2's score
score2 = score2 + 1;
}

if (ball_posX > gb.display.width()) {
// Reset ball
ball_posX = 20;
ball_posY = random(20, gb.display.height() - 20);  // Random position along the Y axis
ball_speedX = 1;

if (random(0, 2) == 1) {  // 50% of the time, this is true
ball_speedY = 1;
}
else {  // Other 50% of the time
ball_speedY = -1;
}

// Increment player 1's score
score1 = score1 + 1;
}

// Draw ball
gb.display.fillRect(ball_posX, ball_posY, ball_size, ball_size);

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

// Draw difficulty
gb.display.setCursor(33, gb.display.height() - 5);
if (difficulty == 3) {
gb.display.print("EASY");
}
else {
gb.display.print("HARD");
}
}

```

Good job you finished the workshop of simple computer controlled enemies :D

By Julien Giovinazzo

Any questions / comments / suggestions, be sure to drop a comment down below!

NEW 4 months ago

OK, je vais faire 2 créations pour ces tutos  ;)

NEW 4 months ago

jicehel

Hello !

On travaille sur la suite de notre côté, on va mettre ça en ligne très bientôt.

Mais ça ne t'empêche pas de faire des tutos, bien au contraire, tu devrais faire des Créations pour qu'il soient mieux mis en avant et que les personnes puissent réagir directement :D

NEW 4 months ago

Bon en attendant la réponse, sinon il y aurait une suite pour changer un peu du Pong avec un Sokoban. Pour ne pas faire trop long, il serait bien sûr divisé en morceaux. Le premier sur la réflexion sur le jeu et sur les tableaux, la seconde partie serait sur les graphiques avec déplacements et la 3ème serait l'organisation du programme en onglet pour s'y retrouver plus facilement et la fin de la création du jeu.

Dis moi si ça t’intéresse et si je fais la suite. En attendant, voici le tuto sur la partie 1 de la création d'un Sokoban

## Partie 1 : Préparons notre jeu

Durée: 30 minutes (à la louche)

Niveau: ancien débutant ayant suivi les premiers tutos

Prérequis

• Avoir une Gamebuino META
• Avoir fait l'atelier Installation de la Gamebuino META
• Avoir suivi les tutoriels jusqu’au Pong pour avoir les bases

## Réfléchissons sur ce que nous voulons faire

Le jeu de Sokoban est un jeu où l’on est le gardien d’un entrepôt où se trouve, dans un petit labyrinthe dans lequel se trouve des caisses que l’on doit ranger à certains emplacements.

C’est un jeu classique, on a donc plein d’exemple de ce jeu. Selon moi, ce genre de jeu avec de principes simples est idéal pour commencer à essayer d’atteindre un objectif, mais bien sûr on peut aussi commencer un jeu sans modèle si l’on est créatif.

Dans un premier temps renseignons nous sur ce que l’on aura à faire :

Contrôler un personnage qui pourra se déplacer dans le labyrinthe s’il n’y a ni mur ni caisse pour le bloquer.

Le joueur pourra pousser une caisse s’il n’y a rien derrière cette caisse

Le niveau est terminé quand toutes les caisses sont sur les emplacements

Bon on va déjà se concentrer sur ces 3 éléments et voir les autres points sur lesquels on peut avoir à réfléchir.

Par exemple :

Est-ce que l’on dessine les niveaux ou est-ce que l’on utilise des sprites ?
Pour cet exemple, on va utiliser des sprites pour intégrer la couleur plus facilement. Pour les couleurs, on utilisera les couleurs de la palette de la META pour répondre à la charte de qualité.

Observons un jeu de Sokoban déjà existant :

On voit que le jeu peut être représenté par une grille de positions. Chaque case de cette grille pouvant contenir quelque chose : Un mur, le personnage, une caisse ou un point d’arrivée. On fera attention toutefois, une caisse peut être sur un point d’arrivée (c’est même le but du jeu) mais si on la déplace de nouveau, on doit pouvoir retrouver le point d’arrivée.

## Commençons a écrire notre jeu

Nous avons vu dans le tuto « Hello World » la structure d’un programme et le principe des 2 fonctions : setup() et loop()

```#include <Gamebuino-Meta.h>

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

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

// C'est ici que le plus gros du programme se déroule
}

```

Que doit faire la boucle loop ?

• Surveiller l’appui sur les touches
• Faire respecter la logique du jeu
• Afficher les graphismes
• Jouer les bruitages

OK, sinon, il faut que l’on définisse notre niveau.

Pour ça on va définir sa taille en nombre de lignes et en nombre de colonnes à l’aide de la directive #define

En regardant sur Wikipedia sur le sujet du Sokoban, on apprend que l’on peut échanger de niveaux via des fichiers au format .xsb. Il y a même le premier niveau de la première version du jeu que nous allons utiliser pour notre programme :

```    #####
#   #
#\$  #
###  \$##
#  \$ \$ #
### # ## #   ######
#   # ## #####  ..#
# \$  \$          ..#
##### ### #@##  ..#
#     #########
#######```

Comptons les lignes et les colonnes de ce niveau : On a 11 lignes et 19 colonnes

On écrira donc

```#define NB_LIGNES_NIVEAUX 11
#define NB_COLONNES_NIVEAUX 19```

```Légende :
# : mur
\$ : caisse
. : destination
* : caisse sur une zone de rangement (pas présente dans ce niveau)
@ : personnage
+ : personnage sur une zone de rangement (pas présent dans ce niveau)```

Tiens, on découvre donc sur Wikipédia comment ils ont résolu le problème du stockage de l’information quand on pousse une caisse sur une destination : ils remplacent le caractère tout simplement et si on retire la caisse, on remettra le « . » dans la case de la destination et « \$ » sur la case contenant la caisse si ce n’est pas une zone de rangement (auquel cas il faudra mettre un « * ».

On remarque aussi le « + » et oui on avait oubli un cas : le personnage peut lui aussi se trouver sur une case de destination ponctuellement. Ils ont donc également ajouté le caractère « + » qui reprend les codes « @ » et « . »

Bien, bien, on avance. C’est une étape importante avant de coder. Bien penser à ce que l’on veut faire et lister les étapes et réfléchir à comment on va le faire.

Pour que notre niveau soit compatible avec les xsb, nous allons stocker les caractères dans un tableau de caractères que l’on va appeler « niveau » vu notre imagination débordante

On appelle tableau une variable composée de données de même type, stockée de manière contiguë en mémoire (les unes à la suite des autres).

 donnée donnée donnée ... donnée donnée donnée

Lorsque le tableau est composé de données de type simple, on parle de tableau monodimensionnel (ou vecteur). Sa syntaxe est la suivante :

type Nom_du_tableau [Nombre d'éléments]

type définit le type d'élément contenu dans le tableau définissant donc la taille d'une case du tableau en mémoire

Nom_du_tableau est le nom que l'on décide de donner au tableau, le nom du tableau suit les mêmes règles qu'un nom de variable

Nombre d'éléments est un nombre entier qui détermine le nombre de cases que le tableau doit comporter

Voici par exemple la définition d'un tableau de 8 éléments de type char :

`char Tableau [8];`

On peut également initialiser un tableau lors de sa définition ainsi:

type Nom_du_tableau [Taille1][Taille2]...[TailleN] = {a1, a2, ... aN};

Pour le tableau de 8 caractères, on pourrait écrire par exemple

`char Tableau [8] = {'A',’B ', 'C', 'D', 'E', 'F', 'G', 'H'};`

Lorsqu’un tableau contient lui-même d'autres tableaux on parle alors de tableaux multidimensionnels (aussi matrice ou table). Il se définit de la manière suivante :

type Nom_du_tableau [a1][a2][a3] ... [aN]

Chaque élément entre crochets désigne le nombre d'éléments dans chaque dimension

Le nombre de dimensions n'est pas limité

Un tableau d'entiers positifs à deux dimensions (3 lignes, 4 colonnes) se définira avec la syntaxe suivante :

`int Tableau [3][4];`

On peut représenter un tel tableau de la manière suivante :

 Tableau[0][0] Tableau[0][1] Tableau[0][2] Tableau[0][3] Tableau[1][0] Tableau[1][1] Tableau[1][2] Tableau[1][3] Tableau[2][0] Tableau[2][1] Tableau[2][2] Tableau[2][3]

Voilà, on a la solution pour notre tableau pour notre niveau. Nous voulons un tableau qui va contenir un caractère correspondant au contenu de chaque case de notre niveau. Nous allons donc créer une matrice de caractère (type char). Nous l’appellerons niveau pour lui donner un nom parlant et ses dimensions seront NB_LIGNES_NIVEAUX x NB_COLONNES_NIVEAUX afin de pouvoir accéder à chacune des cases de notre niveau. On le déclarera donc ainsi :

```char niveau[NB_LIGNES_NIVEAUX][NB_COLONNES_NIVEAUX] =
{
{ ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', ' ', ' ', '#', ' ', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', ' ', ' ', '#', '\$', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', '#', '#', '#', ' ', ' ', '\$', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', '#', ' ', ' ', '\$', ' ', '\$', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ '#', '#', '#', ' ', '#', ' ', '#', '#', ' ', '#', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', },
{ '#', ' ', ' ', ' ', '#', ' ', '#', '#', ' ', '#', '#', '#', '#', '#', ' ', ' ', '.', '.', '#', },
{ '#', ' ', '\$', ' ', ' ', '\$', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '.', '.', '#', },
{ '#', '#', '#', '#', '#', ' ', '#', '#', '#', ' ', '#', '@', '#', '#', ' ', ' ', '.', '.', '#', },
{ ' ', ' ', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', '#', '#', '#', },
{ ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', }
};```

La première accolade indique que l’on initialise le tableau et chaque ligne en dessous correspond à un tableau avec le contenu de cette ligne dans notre tableau. On ferme ensuite notre tableau de lignes.

Attention n’oubliez pas que les tableaux commencent à l’indice 0, donc la première ligne à le numéro 0 et sa 3ème case à le numéro 2. Pur charger la variable Résultat avec le contenu de la 3ème colonne de la ligne 1, je devrais donc écrire :

`Résultat = niveau[0][2] ;`

## A vous de jouer

Bon assez parlé, à vous de travailler. Faites un petit programme qui charge le tableau et affiche le contenu au format texte.

Attention, limitez le nombre de ligne affichée à 10 pour que ça tienne à l'écran, sinon ce ne sera pas joli...

## SOLUTION

```#include <Gamebuino-Meta.h>

#define NB_LIGNES_NIVEAUX 11
#define NB_COLONNES_NIVEAUX 19
int NB_Lignes;
char niveau[NB_LIGNES_NIVEAUX][NB_COLONNES_NIVEAUX] =
{
{ ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', ' ', ' ', '#', ' ', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', ' ', ' ', '#', '\$', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', '#', '#', '#', ' ', ' ', '\$', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', '#', ' ', ' ', '\$', ' ', '\$', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ '#', '#', '#', ' ', '#', ' ', '#', '#', ' ', '#', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', },
{ '#', ' ', ' ', ' ', '#', ' ', '#', '#', ' ', '#', '#', '#', '#', '#', ' ', ' ', '.', '.', '#', },
{ '#', ' ', '\$', ' ', ' ', '\$', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '.', '.', '#', },
{ '#', '#', '#', '#', '#', ' ', '#', '#', '#', ' ', '#', '@', '#', '#', ' ', ' ', '.', '.', '#', },
{ ' ', ' ', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', '#', '#', '#', },
{ ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', }
};
void setup() {
gb.begin();
}
void loop() {
while(!gb.update());
gb.display.clear();
// Astuce: avec print sans rien modifier, par défaut, on ne peut afficher que 10 lignes, pour l'affichage, on va donc brider le nombre de lignes à 10
// On a de la chance on a moins de 20 colonnes. On n'a donc pas besoin de brider le nombre de colonnes à afficher dans notre programme de test
NB_Lignes = NB_LIGNES_NIVEAUX ;
if(NB_Lignes > 10) NB_Lignes = 10;
// On affiche le contenu de chacune des cases
for (int ligne=0;ligne<NB_Lignes;ligne++) {
for (int colonne=0;colonne<NB_COLONNES_NIVEAUX;colonne++) {
gb.display.printf("%c",niveau[ligne][colonne]);
}
gb.display.println("");
}
}
```