1. Introduction

Le "Snake" (littéralement "serpent" en anglais) est un sorte de jeu vidéo dans lequel le joueur contrôle une ligne qui grandit et devient ainsi elle-même son propre obstacle. Le concept vient du jeu d'arcade initialement nommé "Blockade" conçu en 1976 par Gremlin Industries, seulement 4 ans après le coup d'envoi de l'industrie vidéoludique donné par "Pong", et 2 ans avant la sortie de "Space Invader" qui marque le début de l'âge d'or des jeux vidéos. Cela en fait un précurseur, dans les premiers commercialisés, et une licence mondialement connue, considérée comme un classique, notamment grâce à son intégration dans les produits Nokia. Mon objectif était de reproduire physiquement cette dernière version en utilisant le robot Thymio comme tête du serpent.

Blockade, jeu d'arcade créé en 1972.

2. Matériel et méthodes

2.1 Matériel

Pour mon projet, j'ai eu besoin de:

Matériel informatique:
- 1x Thymio
- 1x Télécommande

Pour la confection du plateau de jeu:
- Feuilles de papier A4
- Scotch brun
- Autocollants noirs
- Scotch double face

J'ai créé mon plateau de jeu moi-même, en utilisant une feuille A4 et 8 autocollants noirs par case délimitée par du scotch brun en plastique réfléchissant donc partiellement la lumière. Pour ce faire, j'ai pris en compte les mensurations du robot Thymio; une largeur de case devait être supérieure au double de la distance entre le centre de rotation de Thymio (le trou en son milieu) et ses capteurs de proximités dirigés vers le sol (ce qui équivaut au diamètre du cercle de rotation). J'ai placé les autocollants aux quatre endroits au-dessus desquels se positionnent ces derniers quand Thymio est sur les axes x et y, vers le positif et le négatif (c'est-à-dire quatre directions différentes), son centre de rotation placé au milieu de la case (croisement de ses diagonales). J'en ai utilisé deux à chaque fois, pour être sûre qu'ils soient détectés en cas d'imprécision dans les mouvements du robot. Tous les repères d'une même direction sont théoriquement alignés, mais comme mon tableau est manufacturé, il est certainement relativement imprécis sur de tels points. J'ai ensuite découpé mes cases pour les rendre carrées, puis je les ai assemblées grâce à du scotch double face, et enfin je les ai nommées en fonction de mes axes, la case (0 ; 0) se trouvant au coin inférieur gauche, l'axe x positif de bas en haut, et l'axe y positif de gauche à droite. Le plateau de jeu final, utilisé pour tester le code, comporte 9 cases, arrangé en 3 x 3.


Figure 1: Le plateau de jeu et légendes

Figure 2: La télécommande et la fonction de ses boutons

2.2 Méthode


Voilà un schéma précoce de mon projet:


J'ai défini des règles de jeu précises, afin de mieux en sortir mes objectifs et comprendre les contraintes auxquelles j'allais être soumise pour le codage, les voici:
- Thymio avance continuellement sur le plateau de jeu.
- Le joueur peut lui ordonner grâce à une télécommande de tourner à gauche ou à droite. Sans ordre, il continue tout droit.
- Il y a constamment un point présent à une position aléatoire du plateau.
- À chaque fois que le point est mangé, la queue grandit de 1, et un nouveau point apparaît.
- Si Thymio rentre dans un mur, la partie est perdue.
- Si Thymio rentre dans sa queue, la partie est perdue.

J'ai pu en déduire les problèmes suivants:
- Le robot Thymio n'a pas de géolocalisation incluse, et pourtant j'avais besoin qu'il soit conscient de sa position relative au plateau de jeu, pour qu'il réagisse correctement lorsqu'il atteindrait un point, un bord de case, ou sa queue pour exemple.
- J'avais besoin que Thymio fasse des virages à 90 degrés précis, afin de ne pas dévie sa trajectoire sur le plateau de jeu. La puissance et vitesse des moteurs étant très variables selon la batterie du robot, je ne pouvais pas me baser sur leur constance pour les coder.
- Une fois codés, les objets virtuels du jeu tels la queue et le point à manger sont "visibles" pour le robot, c'est-à-dire qu'il réagit à leur présence, mais le joueur étant bel et bien humain, je devais trouver un moyen de représenter visuellement leur existence, ou du moins d'informer le joueur de leur position d'un moyen ou d'un autre.
- Le joueur devait pouvoir contrôler le lancement du jeu et les mouvements de Thymio avec la télécommande.

Passer une case
Pour chacun de ces problèmes j'ai du trouver une solution. La première fut de construire mon propre plateau de jeu, selon le processus décrit dans la partie "matériel". Cela devait permettre l'utilisation des capteurs de proximité pour informer Thymio quand il passait une case, ou le moment auquel il devait appliquer un ordre reçu de la part du joueur. Le fond blanc des cases, le scotch brun des bords, et les repères noirs ne réfléchissant pas la lumière de la même manière, ils donnaient donc chacun une valeur de proximité différente (fond blanc > scotch > repères). Pour ces deux dernières, j'ai instauré deux constantes, respectivement "scotch" et "reperes". J'ai ensuite créé un tableau de variables à deux valeurs nommé "position", qui indiquait respectivement le x et le y et la case sur laquelle devait se trouver Thymio, ainsi que la variable "direction", augmentée de un à chaque virage vers la droite, et diminué de un pour chaque virage vers gauche. En appliquant à cette valeur l'opération modulo quatre, cela devait indiquer au robot son orientation, et donc la manière dont il allait affecter les valeurs de "position" à chaque fois qu'il passait une case. Pour que cette stratégie marche, il était nécessaire que Thymio soit toujours placé sur la même case et dans la même direction lors du lancement de la partie, et j'ai donc défini comme case de lancement le coin inférieur gauche de coordonnées (0 ; 0).

La queue virtuelle de Thymio
J'ai instauré le tableau de variables "queue" à 32 potentielles valeurs. La première d'entre elle est toujours égale à 1 additionné à la valeur x de "position", le tout divisé par 1 additionné à la valeur y de "position" et à la valeur y d'"echiquier" (présentée plus tard). Cela pour que le dividende ne soit jamais égal au diviseur, afin d'éviter des valeurs de queue identiques pour deux cases différentes. Cette valeur devait donc toujours correspondre à la position en temps réel de la tête du serpent; Thymio. Lorsqu'une case est passée, toutes les valeurs de ce tableau sont déplacées vers la droite, et le première est remplacée par la nouvelle position de Thymio. Devait être ainsi créé une mémoire des cases par lesquelles Thymio est récemment passé. En revanche, je n'étais pas certaine de comment opérer ce décalage des valeurs. J'ai trouvé ">>=", mais je ne pense pas l'avoir utilisé correctement.

Pour les deux sections ci-dessus:

var position[2]
var direction
var queue[32]
var echiquier[2]

position[0] = 0
position[1] = 0
direction = 0

onevent prox

 if reperes < prox.ground.reflected[0]
	and prox.ground.reflected[0] < scotch 
	and reperes < prox.ground.reflected[1] 
	and prox.ground.reflected[1] < scotch 
		then if direction%4 == 0 then position[0] = position[0]+1 #axe +x 
			elseif direction%4 == 1 or direction%4 == -3 then position[1] = position[1]+1 #axe +y 
			elseif abs(direction%4) == 2 then position[0] = position[0]-1 #axe -x 
			elseif direction%4 == 3 or direction%4 == -1 then position[1] = position[1]-1 #axe -y 
end 

queue[0] >>= (position[0]+1)/(position[1]+echiquier[1]+1)
end 

Rentrer dans un mur
J'ai instauré le tableau de variables à deux valeurs "echiquier", qui étaient respectivement égales à la valeur maximum de l'axe x et de l'axe y imposées par la grandeur physique du plateau de jeu. Pour celui de 3 x 3 que j'ai utilisé pour tester mon code, les deux valeurs seraient donc égales à 2. Je les ai utilisées comme maximums pour celles de "position". En effet, si celles-ci surpassaient respectivement les valeurs d'"echiquier", ou étaient inférieur à 0 (car les coordonnées du plateau de jeu ne descendent pas en dessous de 0), cela aurait signifié que Thymio serait rentré dans un mur virtuel, c'est-à-dire une des extrémités physiques du plateau de jeu.

var echiquier[2]

echiquier[0] = 2
echiquier[1] = 2

if position[0] >= echiquier[0] 
or position[0] <= 0 
or position[1] >= echiquier[1] 
or position[1] <= 0 
then callsub gameover 

end 

Sous-routine gameover
Comme indiqué dans la section de code ci-dessus, j'ai aussi créé la sous-routine appelée "gameover". Elle devait intervenir à chaque fois que la partie était perdue, c'est-à-dire quand le serpent rentre dans un mur ou dans sa queue. J'ai décidé qu'à chaque mort, Thymio, après s'être complètement arrêté, s'allumerait en rouge et émettrait un son aigu pour 3 secondes, puis afficherait grâce aux LEDs de proximité horizontales le score en binaire (variable "score") défini par le nombre de points mangés durant la partie, et grâce aux LEDs autour des boutons la distance parcourue en binaire (variable "distance"), définie par le nombre de cases passées par Thymio durant la partie, et finalement s'allumerait d'une couleur entre le rouge et le vert, jugeant de la qualité de la partie, selon le score. (Pour score == 0, thymio serait uniquement rouge, puis tirerait progressivement vers le vert jusqu'à score >= 16, valeur pour laquelle Thymio serait uniquement vert.)

var score
var distance

sub gameover 
	motor.left.target = 0
	motor.right.target = 0 
	timer.period[0] = 3000
		call leds.top(32,0,0) 
		call sound.freq(2000,180)
		onevent timer0
	timer.period[0] = 0
		call leds.top(0,0,0) 
		call leds.prox.h(32*((score &  1)>>0), 
				  32*((score &  2)>>1), 
				  32*((score &  4)>>2), 
				  32*((score &  8)>>3), 
				  32*((score & 16)>>4), 
				  32*((score & 32)>>5), 0, 0)
		call leds.circle(32*((distance &  1)>>0), 
				  32*((distance &  2)>>1), 
				  32*((distance &  4)>>2), 
				  32*((distance &  8)>>3), 
				  32*((distance & 16)>>4), 
				  32*((distance & 32)>>5), 
				  32*((distance & 64)>>6), 
				  32*((distance & 128)>>7))
		call leds.top((32-score-16),(score+16),0)

Effectuer les virages selon les ordres de la télécommande
Pour que Thymio effectue les virages ordonnés par le joueur grâce à la télécommande, j'ai du utiliser la variable "rc5.command" définie par la télécommande. J'ai arbitrairement attribué le bouton "4" (rc5.command=4) à l'ordre tourner à gauche, et le bouton "6" (rc5.command=6) à l'ordre tourner à droite à cause de leur emplacement stratégique sur la télécommande (voir Figure 2). Lorsque rc5.command==5, Thymio doit continuer tout droit, sachant que rc5.command retourne à cette valeur après chaque virage effectué. Les potentiels ordres sont effectués quand le capteur gauche de proximité au sol voit un repère, c'est-à-dire que le centre de rotation de Thymio est aligné avec le centre de la case. Un virage est donc engagé, et celui-ci se termine quand ce même capteur de proximité au sol rencontre le prochain repère de la case, et Thymio repart tout droit. Pour faire la différence lors de la condition entre le repère de début et de terminaison de virage, j'ai créé la variable "virage". Celle-ci est égale à 1 ou 0 selon si Thymio est en train d'effectuer un virage. Tout cela sans oublier d'affecter la variable "direction" comme indiqué dans la section "passer un case". Pour des raisons de facilité j'ai également regroupé le retour à un déplacement vers l'avant, l'attribution rc5.command=5 et celle virage=0 dans une sous-routine appelée "straightahead" (littéralement "tout droit" en anglais). Celle-ci inclut la variable "vitesse" qui est définie par la vitesse indiquée à Thymio lorsqu'il va tout droit.

var direction
var virage 
var vitesse

virage = 0 
vitesse = 200

onevent prox

if prox.ground.reflected[0] < reperes and virage == 0 then 
	if  rc5.command == 5
		then callsub straightahead
	elseif rc5.command == 4
		then motor.left.speed  = -200
		motor.right.speed = 200
		direction --
		virage ++
			if  prox.ground.reflected[0] < reperes and virage == 1
			then callsub straightahead
			end
	elseif  rc5.command == 6 
		then motor.left.speed  = 200
		motor.right.speed = -200
		direction ++
		virage ++
			if  prox.ground.reflected[0] < reperes and virage == 1 
			then callsub straightahead
			end
	end 
end

sub straightahead
  motor.left.target = vitesse
  motor.right.target = vitesse
  virage = 0
  rc5.command = 5

Rentrer dans la queue
Nous avons défini tout à l'heure le tableau de variables "queue" en tant que mémoire des cases par lesquelles Thymio est passé. L'intérêt d'en avoir une était celui de définir si ce dernier rentre dans sa queue et perd le jeu ou non. C'est le cas si le tableau de variables "queue" contient 2 valeurs identiques dans l'intervalle des valeurs de "queue" de 0 à "score" (nombre de points mangés). En effet, pour score==n (la queue fait donc une taille de valeur n+1), les valeurs de "queue" ne seront prises en compte que jusqu'à la n-ième valeur. Je ne sais cependant pas comment exprimer cela simplement, et j'ai donc écrit les conditions pour chaque valeur de "score" entre 4 et 8, le première car un serpent d'une taille de 4 ne peut pas rentrer dans sa propre queue, et la seconde car le plateau de jeu que j'ai ne comporte que 9 cases, et la queue du serpent ne peut donc pas surpasser ce nombre. J'ai quand même écris la condition pour score==n dans la théorie d'un plateau de jeu contenant un nombre de cases égal à (n+1) (et à donc la taille maximum théorique de la queue).

var score
var queue[n+1]

if score == 4 and (queue[0]==queue[4]) then callsub gameover
	elseif score == 5 and (queue[0]==queue[4] or queue[0]==queue[5]) then callsub gameover 
	elseif score == 6 and (queue[0]==queue[4] or queue[0]==queue[5] or queue[0]==queue[6]) then callsub gameover 
	elseif score == 7 and (queue[0]==queue[4] or queue[0]==queue[5] or queue[0]==queue[6] or queue[0]==queue[7]) then callsub gameover 
	elseif score == 8 and (queue[0]==queue[4] or queue[0]==queue[5] or queue[0]==queue[6] or queue[0]==queue[7] or queue[0]==queue[8]) then callsub gameover
[etc....until]
	elseif score == n and (queue[0]==queue[4] or queue[0]==queue[5] or [etc... until] or queue[0]==queue[n]) then callsub gameover 

end

Générer aléatoirement un point et le manger
Pour que la queue du serpent grandisse d'une case, Thymio doit passer par la case qui contient un point unique pour un moment donné, c'est-à-dire le "manger". Ceci fait, un autre point apparaît à un nouvel emplacement aléatoire du plateau de jeu. Ses coordonnées sont données par le tableau de variables à deux valeurs "point" auquel j'impose un minimum et un maximum, qui sont les extrémités des axes (x;y]) du plateau de jeu, grâce à la fonction native "math.argbounds". Une autre fonction native "math.rand" va générer aléatoirement les deux valeurs de "point", qui seront, je l'espère, car je ne suis pas exactement certaine de l'utilisation de ces deux fonctions, comprises entre les intervalles que je viens de définir. La sous-routine "poof" les regroupe.

var position[2]
var point[2]

callsub poof

if position[0] == point[0] and position[1] == point[1]
	then score ++
	callsub poof 
end 

sub poof
	call math.argbounds(point[0],0,echiquier[0])
	call math.argbounds(point[1],0,echiquier[1])
	call math.rand(point[0])
	call math.rand(point[1])

Afficher les coordonnées du point à manger
Le point à manger existe donc virtuellement, mais je joueur a besoin d'être informé de son positionnement sur le tableau de jeu pour aller le récupérer avec Thymio. J'ai donc décidé d'utiliser les six LEDs de proximité horizontales à l'avant du robot pour indiquer, en binaire, sur les trois de gauche la coordonnée x de "point", et sur les trois de droite sa coordonnée y. Le joueur doit donc savoir lire le binaire de 0 à 7 rapidement pour jouer correctement, et cela limite aussi les valeurs d'"echiquier" à 7, pour un maximum de 64 cases dans le cas d'un plateau de jeu de dimensions 8 x 8.

var point[2]

call leds.prox.h(32*((point[0] &  1)>>0), 
	   		 32*((point[0] &  2)>>1), 
			 32*((point[0] &  4)>>2), 
			 32*((point[1] &  1)>>0), 
			 32*((point[1] &  2)>>1), 
			 32*((point[1] &  4)>>2), 0, 0)

Les loops d'états
J'étais relativement perdue quand j'en suis venue à la question d'harmoniser mon code pour le rendre réellement lisible pour la machine, mais j'avais déjà constaté l'utilité d'instaurer une variable "etat" qui définirait trois comportements différents: si etat==0, le jeu est prêt au lancement, toutes les variables réinitialisées, et Thymio doit être aligné parfaitement à l'axe +x sur la case (0 ; 0), le milieu de son cercle de rotation aligné au centre de celle-ci. Quand etat==0, cela veut dire que le jeu est en cours, et finalement quand etat==2, c'est que la partie est terminée, Thymio a appliqué la sous-routine "gameover" et affiche les score et la distance parcourue. En parcourant le site officiel du thymio, j'ai découvert qu'il était possible de créer des boucles d'instructions, et j'ai trouvé avisé de créer une boucle pour chacun de ces trois états. J'ai harmonisé et unifié mon code grâce à cela. Pour passer d'une boucle à l'autre, il faut: De etat==0 à etat==1: appuyer le bouton "5" de la télécommande (rc5.command=5) qui appelle la sous-routine "straightahead" dans laquelle j'ai rajouté l'instruction "etat = 1", ce qui lance la partie et démarre le Thymio (voir Figure 2).

var etat

onevent rc5

etat = 0
if rc5.command == 5 
	then callsub straightahead end

sub straightahead
  etat = 1
  motor.left.target = vitesse
  motor.right.target = vitesse
  virage = 0
  rc5.command = 5

De etat==1 à etat==2: rentrer dans un mur ou rentrer dans la queue, car cela appelle la sous-routine "gameover" à laquelle j'ai rajouté l'instruction "etat=2" pour les deux cas, mais également appuyer sur le bouton "8" de la télécommande (rc5.command=8) qui va simplement appeler cette même sous-routine et donner la possibilité au joueur d'interrompre sa partie à tout moment (voir Figure 2).

var etat

onevent rc5

etat = 1
if  rc5.command == 8
	then callsub gameover end

sub gameover
	etat = 2
	#+ le même code que dans la section "sous-routine gameover"

Et de etat==2 à etat==0: appuyer sur le bouton "2" de la télécommande (rc5.command=2), ce qui va simplement appliquer "etat=0" (voir Figure 2).

var etat

onevent rc5

etat = 2
if rc5.command == 2
then etat = 0 end

Choisir la vitesse avant le lancement du jeu
J'ai mentionné la variable "vitesse" à plusieurs reprises dans les sections ci-dessus. J'en ai fais une variable et non une constante dans l'idée de pouvoir la choisir quand etat==0, donc pendant la préparation au lancement du jeu. En appuyant sur le bouton central sur le Thymio, cela ajoute 50 à la variable vitesse, et dès que celle-ci dépasse 500, la valeur maximum pour la vitesse, elle est automatiquement retournée à 100. Cela laisse 9 différentes vitesses possibles. Elles sont visuellement représentées par les LEDs autour des boutons(leds.circle). Quand aucune des celles-ci n'est allumée, vitesse==100, et une nouvelle LED s'allume à chaque fois que 50 est ajouté à la vitesse, jusqu'à ce que vitesse==500, et que toutes les LEDs soient allumées.

var etat
var vitesse

onevent button.center

etat == 0
vitesse = 100
if button.center == 1 then vitesse = (vitesse+50) end
		if  vitesse > 500 then vitesse = 100 end	
		
	 		call leds.prox.h(32*(((vitesse/50) &  1)>>0), 
					 	 32*(((vitesse/50) &  2)>>1), 
						 32*(((vitesse/50) &  4)>>2), 
						 32*(((vitesse/50) &  8)>>3), 
						 32*(((vitesse/50) & 16)>>4), 
						 32*(((vitesse/50) & 32)>>5), 
						 0, 0)
			call leds.circle(32*(((vitesse-149)>0) & 1), 
						 32*(((vitesse-199)>0) & 1), 
						 32*(((vitesse-249)>0) & 1), 
						 32*(((vitesse-299)>0) & 1), 
						 32*(((vitesse-349)>0) & 1),
						 32*(((vitesse-399)>0) & 1), 
						 32*(((vitesse-449)>0) & 1), 
						 32*(((vitesse-499)>0) & 1))

3. Résultats

Mon code ne marche pas, et c'est désolant. Thymio refuse d'appliquer plus loin que le lancement, c'est-à-dire qu'il part tout droit, et ne répond pas aux ordres, même si les valeurs de proximité et de rc5.command montrent bien qu'il a entendu les consignes et que les conditions ont été remplies pour les appliquer. Parfois il commence à tourner sur lui-même, et ne s'arrête plus. La dernière fois que je l'ai testé, alors que le code est dans son état de rendu, il lançait pour une raison obscure la sous-routine "gameover" alors que j'appuyais sur le bouton 5 en etat==0, ce qui était sensé lancer le jeu.

4. Discussion

Quand j'ai demandé conseil au maître d'informatique, il m'a fait remarquer qu'utiliser des boucles en Aseba est une très mauvaise idée, car c'est de la programmation événementielle et non linéaire. Le robot se retrouve confus avec à gérer les événements et les boucles, et n'est pas capables d'appliquer les deux simultanément. Ce qui veut dire que pour avoir un code fonctionnel, je devrais complètement réorganiser ma programmation en fonction des événements et éliminer toutes les boucles auxquelles j'ai eu recours. Je pourrais bien-sûr réutiliser beaucoup de parties de mon codes telles-quelles ou presque, mais cela reste un gros effort.

De plus, il est certain que de nombreux problèmes sont cachés par ce plus gros problème qu'est ma programmation de style linéaire, et dès lors que j'aurai tout converti en programmation événementielle, ils se feront évidents. Notamment je prédis que l'utilisation des fonctions natives "math.argbounds" et "math.rand" fera un soucis, car leur l'utilisation correcte m'est inconnue. Je les soupçonne d'ailleurs des derniers maux en date de mon projet. La vitesse dans les virages a aussi un grand impact sur le bon alignement de Thymio sur les axes du plateau de jeu, donc les virages à 90 degrés, c'est à une vache près, hein, c'est pas une science exacte, surtout quand une petite baisse de batterie affecte autant la précision des moteurs intégrés au robot. Ce point me fait douter de si mon jeu sera jamais jouable. Et encore bien d'autres points que je suis pas en mesure de prédire maintenant sauraient se dévoiler.

5. Conclusion

Pour arriver à un jeu fonctionnel, j'ai montré qu'il aurait fallu complètement réorganiser mon code ainsi que résoudre les problèmes jusque là cachés qui seront révélés par une programmation événementielle plus adéquate. Si j'avais eu deux semaines supplémentaires pour ce faire, j'aurai eu le temps d'y procéder, mais étant surchargée dans toute mes matières gymnasiales au même moment et étant dotée d'une organisation personnelle plus que douteuse, ainsi qu'une vilaine manie de m'y prendre à la dernière pour mes projets, ça n'est malheureusement pas possible pour le rendu. Je suis cela dit très intéressée à retoucher mon code pour mon plaisir personnel, car prendre part à ce projet m'a certainement divertie et surtout beaucoup appris, malgré un résultat non-fonctionnel, et me cependant laisse un amer sentiment de frustration et un grand désir de revanche.

Références

https://segaretro.org/Blockade : image de Blockade https://fr.wikipedia.org/wiki/Histoire_du_jeu_vid%C3%A9o#Premier_jeu_vid%C3%A9o https://fr.wikipedia.org/wiki/Snake_(genre_de_jeu_vid%C3%A9o) https://www.thymio.org/fr:asebalanguage https://www.thymio.org/fr:asebastdnative