random()
La fonction random() que nous allons voir maintenant, permet d'obtenir un nombre entier aléatoire. La référence nous parle d'ailleurs d'un long (pour la valeur retournée). Elle admet deux formes:
random(max); random(min, max);
La première forme retourne un nombre entre 0 et max-1, la deuxième entre min et max-1. Nous allons faire un programme qui affiche toutes les demi secondes un chiffre aléatoire.
Premier programme
Cela devrait se faire aisément, on peut garder la trame de la page précédente, et au lieu d'afficher le résultat de la conversion, on affiche le chiffre aléatoire. Le setup() est identique et se contente d'initialiser la voie série pour l'affichage. Dans loop() on peut trouver par exemple sur deux lignes:
Serial.println(random(10)); // 0 à 9 delay(500); // défilement lent
ou sur trois lignes avec une variable:
byte n = random(10); // 0 à 9 Serial.println(n); delay(500); // défilement lent
Pour la variable, un byte ou un uint8_t est suffisant.
On va en plus en profiter pour afficher une petite explication, genre "Voici un chiffre aléatoire: 7". Dans cet affichage, il y a deux choses complètement différentes: une chaîne fixe et une variable. On peut faire cet affichage avec une seule instruction mais Serial.print() n'admet qu'un seul paramètre. On peut concaténer les deux morceaux en un seul, mais il est déconseillé de le faire pour une histoire compliquée de fragmentation mémoire. On retiendra que si on a plusieurs choses à écrire, on le fera en plusieurs fois. Cela peut donc faire:
Serial.print("Voici un chiffre aléatoire: "); Serial.println(random(10)); // 0 à 9 delay(500); // défilement lent
Notez qu'on ira à la ligne (le ln final après print) que la deuxième fois, et qu'il y a un espace à la fin de la chaîne de caractère pour séparer le chiffre affiché.
Lancez le programme. Aide?void setup() { Serial.begin(115200); // Pour pouvoir utiliser la console } void loop() { Serial.print("Voici un chiffre aléatoire: "); Serial.println(random(10)); // 0 à 9 delay(500); // défilement lent } Si tout va bien, vous devez voir défiler lentement des chiffres.
Relancez plusieurs fois le programme (par exemple en appuyant sur le bouton reset, en débranchant et en rebranchant l'alimentation, en fermant et en rouvrant la console) et notez les premiers chiffres après chaque démarrage. Vous devez observer une chose curieuse: les nombres censés être aléatoires sont toujours les mêmes.
Série pseudo aléatoire
Le C a un peu triché. On ne sait pas calculer une succession de nombres aléatoires. Le C sait juste calculer une série de nombres qui ressemblent à des nombres aléatoires. Voici comment en fait procède random(): à partir d'un nombre (un long), on va calculer par une formule mathématique, le nombre d'après. Pour avoir une idée, imaginez que vous vouliez faire la même chose avec des nombres de 0 à 9999, et que quand on a un nombre ABCD, le suivant serait obtenu en prenant le nombre BDAC auquel on ajouterait 1234. On obtient alors une série prévisible, mais si on ne regarde que les nombres obtenus, ceux-ci semblent ne pas avoir de logique.
random() utilise un long en interne et à chaque appel, un nouveau long est généré, puis est calculé le nombre désiré (dans l'intervalle demandé à partir de ce long).
On parle alors de série pseudo aléatoire, parce qu'elle ressemble à une série aléatoire. En général cela nous suffit. Mais se pose la question du départ. Si on ne sait pas réellement calculer une suite de nombre aléatoire, on ne peut pas non plus choisir le premier au hasard. C'est ce que l'on a vu avec notre programme. Imaginez que l'on fasse un jeu de roulette. Si la série est toujours la même, il est facile de retenir le premier tirage et de gagner à coup sûr la première fois (et on peut apprendre les 10 premiers coups).
Il faudrait que nous donnions au programme un premier nombre de base qui ne soit pas toujours le même.
randomSeed()
C'est cette fonction qui peut initialiser la suite. On lui donne un long entre les parenthèses, elle ne retourne pas de valeurs, et cela permet de choisir la série. Mais il faut trouver un long qui soit différent à chaque fois. L'idéal serait de disposer de l'heure réelle et de donner par exemple le temps en secondes depuis le 1er janvier 2000. On aurait ainsi un nombre de départ qui ne serait jamais le même. Mais tout le monde n'a pas le bon périphérique pour délivrer l'heure.
Une solution qui fonctionne pas trop mal est d'utiliser la lecture de la tension sur la broche A0 (sous réserve que l'on ne l'utilise pas!). On a vu que ce n'est pas bien stable, et que cela donne une valeur qui n'est pas toujours la même. Suivant où on met les doigts, on obtiendra une série parmi 1024 séries différentes. Dans la pratique, on a beaucoup moins de valeurs différentes, et on se contentera d'une série parmi 10 séries. Mais cela est suffisant dans beaucoup de cas. C'est pour cela que l'on voit souvent pour initialiser la série l'instruction:
randomSeed(analogRead(A0));
ou bien:
word valeur = analogRead(A0); randomSeed(valeur);
ou encore:
word valeur; ... valeur = analogRead(A0); randomSeed(valeur);
Rajoutez une de ces trois formes au programme de tout à l'heure et vérifiez que l'on a plus toujours les mêmes valeurs.
Autre méthode
Une autre méthode qui marche bien si on a un bouton poussoir branché sur la carte. Nous ne pouvons pas le faire avec cette partie du tutoriel, mais gardez l'idée sous le coude, elle peut être utile plus tard. Il s'agit simplement de regarder le temps en µs entre le début du programme et le moment ou on appuie sur le bouton, et d'initialiser la série avec ce temps. Ce temps ne peut pas être deux fois le même.
Avec l'EEprom
Ce que je préfère presque c'est de démarrer la première fois avec randomSeed(1); (pas 0 car cela n'initialise rien du tout), la seconde fois avec randomSeed(2);, la troisième fois avec randomSeed(3);... Il nous faut alors une variable que l'on doit conserver d'une fois sur l'autre même quand on coupe l'alimentation. Cela peut se réaliser en en mémorisant ce nombre en EEprom. Gardez cette idée pour votre prochain jeu, pour l'instant je n'ai pas encore parlé de la gestion de l'EEprom.
Pour aller plus loin
Je vous propose un cas limite (le plus mauvais) pour montrer que ce n'est pas vraiment aléatoire. C'est une petite diversion, si vous ne comprenez pas à partir d'ici, laissez tomber, cela ne vous portera pas préjudice pour la suite.
Je vais couper l'espace des long positifs en trois zones sensiblement égales (pas exactement car leur nombre est une puissance de 2 qui n'est donc pas divisible par 3). Je vais demander un nombre aléatoire dans les deux premiers tiers, et je vais compter combien de fois le nombre est dans le premier tiers, et combien de fois le nombre est dans le deuxième tiers.
Voici l'espace des long positifs:
Je coupe l'espace des long positifs en 3 parties:
Le premier tiers va de 0 à 715827882 (715827883 valeurs). Le deuxième tiers va de 715827883 à 1431655765 (715827883 valeurs). Dans le setup(), on initialisera la voie série. vous pouvez utiliser n'importe quelle suite aléatoire, mais cela ne change rien. On n'utilisera pas forcément randomSeed():
void setup() { Serial.begin(115200); }
Dans loop, on va prendre un nombre aléatoire "nombrePseudoAleatoire" entre 0 inclu et 1431655766 exclu. Si ce nombre est dans le premier tiers, on incrémente la variable "premierTiers", si elle est dans le deuxième tiers on incrémente la variable "deuxiemeTiers". Puis on affiche les pourcentages. Cela peut donner:
void loop() { // Calculs nombrePseudoAleatoire = random(1431655766); // On prend le nombre "quelconque" if (nombrePseudoAleatoire <= 715827882) premierTiers++; else deuxiemeTiers++; // affichage Serial.print(premierTiers * 100 / (premierTiers + deuxiemeTiers)); Serial.print("% dans le 1er tiers, "); Serial.print(deuxiemeTiers * 100 / (premierTiers + deuxiemeTiers)); Serial.println("% dans le 2ème tiers, "); delay(500); // Pour ralentir }
Les variables premierTiers et deuxiemeTiers doivent garder leurs valeurs d'une fois sur l'autre. On ne peut pas les déclarer dans loop() car elles serait initialisées à 0 à chaque fois. Pour nombrePseudoAleatoire c'est possible, mais on va tout déclarer au même endroit, avant loop(), éventuellement en tout début, avant setup(). nombrePseudoAleatoire doit être un long, les deux compteurs peuvent être des word si on garde une attente d'une demi seconde sinon, il vaut mieux prendre un unsigned long Cela peut donner:
long nombrePseudoAleatoire; // Nombre aléatoire à tester unsigned long premierTiers; // Nombre de fois que l'on est dans le premier tiers unsigned long deuxiemeTiers; // Nombre de fois que l'on est dans le deuxième tiers
Assemblez les trois morceaux de code, et exécutez-le. Aide?long nombrePseudoAleatoire; // Nombre aléatoire à tester unsigned long premierTiers; // Nombre de fois que l'on est dans le premier tiers unsigned long deuxiemeTiers; // Nombre de fois que l'on est dans le deuxième tiers void setup() { Serial.begin(115200); // Pour pouvoir utiliser la console } void loop() { // Calculs nombrePseudoAleatoire = random(1431655766); // On prend le nombre "quelconque" if (nombrePseudoAleatoire <= 715827882) premierTiers++; else deuxiemeTiers++; // affichage Serial.print(premierTiers * 100 / (premierTiers + deuxiemeTiers)); Serial.print("% dans le 1er tiers, "); Serial.print(deuxiemeTiers * 100 / (premierTiers + deuxiemeTiers)); Serial.println("% dans le 2ème tiers, "); delay(500); // Pour ralentir } Si les nombres étaient aléatoire, on obtiendrait au bout de quelques itération autant de valeurs dans le premier tiers que dans le deuxième et l'affichage donnerait 50% 50%. Or on a 66% et 33%. Si vous pensez que cela vient du programme, essayez avec deux petits intervalles 0-99 et 100-199. Cela vient de la façon dont le calcul des nombres pseudo aléatoire est fait en interne. On sait faire une succession de long pseudo aléatoire, mais on ne saurait pas faire une série de nombres compris entre 0 et 100. les fonctions aléatoires calculent donc une série de long et pour avoir un nombre compris entre 0 et N, on garde le reste de la division du long par N. Tant que N est petit, cela ne se voit pas, mais en prenant le pire des cas, ce que nous avons fait, on voit le problème. Mais ce cas tout à fait particulier ne se rencontre jamais dans la pratique.