Ce tutoriel est structuré autour de la création d'un jeux vidéo simple, le classique Snake. Il existe plusieurs variantes de ce jeu. Comme il s'agit d'un tutoriel sur le langage Forth, cette version sera la plus simple.
Il est préférable de lire la présentation du projet ForthEx avant de lire ce tutoriel ainsi que les 4 premiers paragraphes de l'index principal.
Le pourtour de l'écran est circonscris par une ligne blanche, le serpent est donc limité à une surface de 62 par 22 pixels. Puisqu'il s'agit d'un écran texte, les pixels ici sont représentés par l'espace d'un caractère. Sur la première ligne de l'écran s'affiche le pointage SCORE:, et la longueur du serpent LENGTH:. Ces compteurs augmentent chaque fois qu'il mange.
Essayons de voir de quoi nous avons besoins pour réaliser ce jeu. Notez que le langage ForthEx est insensible à la casse. On peut écrire les identificateurs en minuscule,majuscules ou un mélange des deux. Les indentificateurs sont convertis en majuscules avant d'être ajouter au dictionnaire.
direction | valeur | car. tête |
---|---|---|
est | 0 | < |
sud | 1 | W |
ouest | 2 | > |
nord | 3 | V |
Le corps du serpent et les pastilles de nourritures seront représentés par le caractère O en inverse vidéo.
Dans cette version simple du jeux le serpent va toujours se déplacer à la même vitesse. Le délais de boucle sera donc déterminé
par la constante SPEED.
Voici l'apparance du jeu au démarrage d'une partie. Au centre c'est le serpent qui se dirige vers l'est.
Le langage Forth donne accès à ses structures internes. Il faut donc que le programmeur Forth connaisse le fonctionnement interne, ce qu'on appelle la machine virtuelle. Nous allons donc décrire ici cette machine virtuelle.
Il y a 2 piles que l'utilisateur doit manipuler, la pile des arguments et la pile des retours. Dans la plupart des langages comme le C le programmeur n'a pas besoin de manipuler la pile, c'est le compilateur qui s'en occupe. En Forth c'est différent, vous devez connaître ces 2 piles et les mots Forth qui permettent de les manipuler.
La pile des arguments comme son nom l'indique sert à passer des arguments entre les fonctions et aussi à retourner les valeurs des fonctions. Une foncion peu retourner plus d'une valeur. En plus elle sert à garder les variables temporaires qui sont utilisées par la fonction. Ce n'est pas très différent du langage C sauf que pour ce dernier le programmeur n'a pas besoin de connaître ces détails. Une des difficultés du Forth est que justement vous devez garder en tête l'état le la pile des arguments mais aussi de la pile des retours dans certains cas. L'autre différence entre le C et Forth est qu'en C il n'y a qu'une seule pile pour les arguments et les retours.
La pile des arguments utilise un pointeur nommé SP qui pointe sur l'élément au sommet de la pile. Il y a des mots comme SP@ et SP! qui permettent de lire et initialiser la valeur de ce pointeur respectivement.
La pile des retours est très semblable à ce qu'on retrouve dans d'autre langages comme le C. Sa fonction principale est de conserver les adresses de retour lors d'un appel de sous-routine. En plus de ce rôle on s'en sert aussi pour conserver certaines valeurs afin de faciliter l'accès à des valeurs de la pile des arguments qui ne sont pas au sommet. Plusieurs mots servent à transférer des valeurs entre la pile des arguments et la pile des retours.
La pile des retours utilise un pointeur nommé RP qui lui pointe non pas sur la valeur au sommet de la pile mais sur le premier emplacement vide. Ceci est un détail de l'implémentation de ForthEx et n'affecte en rien l'utilisation des mots Forth. Il est quand même utile de savoir que le mot RP@ retourne une adresse au dessus du sommet de la pile.
Un mot qui revient souvent en Forth est le mot CELL, qui signifie cellule. La cellule est l'élément de base des données, c'est à dire la taille élémentaire d'une donnée sur les piles. Habituellement les 2 piles ont la même taille de cellule. Pour ForthEx une cellule est un élément de 16 bits. Ça signifit que les opérations sur les piles se font sur des objets de 16 bits. Un entier ForthEx a 16 bits, un entier double a 32 bits et occupe donc 2 cellules sur la pile. L'ordinateur ForthEx utilise aussi des pointeurs 16 bits, sauf pour l'accès à certains périphériques de stockages qui nécessitent des adresses de 32 bits. Dans ce cas on utilise un entier double pour les représentés.
L'autre élément fondamental du Forth est ce qu'on appelle le dictionnaire. Le dictionnaire Conserve le nom de tous les symboles utilisés par le langage, les opérateurs arithmétique, logiques, relationels, les fonctions, les variables et les constantes. À chacun des ces symboles est associé un pointeur qui indique l'action qui doit-être exécutée lorsque ce nom est invoqué dans un programme où sur la ligne de commande interactive. Par exemple si on invoque le nom d'une constante sa valeur est empilée au sommet de la pile des arguments. Si on invoque le nom d'une variable l'adresse de celle-ci est empilée au sommet de la pile des arguments. Si on invoque le nom d'un opérateur arithmétique comme + cette opération est exécutée en utilisant les 2 valeurs au sommet de la pile des arguments.
La machine virtuelle ForthEx utilisent un certain nombre de constantes et variables qui sont accessible au programmeur nous les mentionnerons lors de leur utilisation.
Je dirais que ce qui caractérise le plus le langage Forth est la simplicité de la syntaxe. Un programme Forth est simplement une liste de mots séparés par un espace. L'interpréteur lit un mot et le cherche dans le dictionnaire, s'il le trouve il exécute immédiatement l'action qui est associé à ce mot. Si le mot n'est pas dans le dictionnaire ce doit-être un entier (simple ou double) et dans ce cas l'interpréteur empile la valeur de cet entier. L'interpréteur Forth est donc très simple: Lire le prochain mot, le chercher dans le dictionnaire, exécuter son action si trouvé. Sinon vérifier si c'est un nombre et empiler ce nombre autrement arrêter et signaler une erreur.
Lorsqu'on est en train de compiler une nouvelle définition dans le dictionnaire, la seule différence est que l'interpréteur compile l'adresse du code à exécuter au lieu de l'exécuter à moins qu'il s'agisse d'un mot immédiat. Les mots immédiat sont toujours exécutés immédiatement même lors d'une compilation.
Il y a deux types de commentaires en Forth ceux introduit par le mot ( et qui se termine par le délémiteur ) et ceux qui sont
introduit par le mot \ et qui se termine à la fin de la ligne. Notez que j'ai dit qu'il s'agissait de mots Forth. Ça signifit
qu'il faut qu'ils soient séparés par un espace de chaque côté.
Lorsque l'interpréteur ne trouve pas le mot dans le dictionnaire et que ce n'est pas un nombre il s'arrête en affichant le
mot suivit d'un point d'interrogation.
Il existe une forme de commentaire qui sert à décrire en abrégé l'interface d'un mot (fonction). Vous rencontrerez cett forme de commentaires
en lisant la description de chaque mot du langage ForthEx. Voici un exemple:
Le commentaire ( n1 n2 -- u ) nous informe que la fonction xy>ucoord accepte comme arguments 2 entiers et retourne
un entier non signé.
Une autre forme de commentaire nous aide à nous rappeller ce qui se trouve sur la pile des arguments. Voici la définition du même mot avec
ce type de commentaire.
L'argument n1 représente une coordonnée x, l'argument n2 représente une coordonnée y et l'entier non signé final
est une version compressée du couple {x,y} appellé ucoord. S: représente la pile des arguments et ce qui suis son contenu.
La valeur la plus à droite est au sommet de la pile.
Puisqu'en Forth seul l'espace est utilisé comme séparateur de mot, un mot peut être formé avec n'importe quel caractère sauf l'espace.
Autre particularité de la syntaxe Forth est que les arguments ( sauf exception ) sont énoncés avant la fonction. C'est ce qu'on appelle la
notation préfixée. exemple.
À ce point çi nous disposons suffisamment d'information pour débuter l'écriture du code. Si nous avons manqué un point nous nous ajusterons en cours de route. Mais d'abord il faut répondre à quelques questions.
Commençons donc à créer notre programme snake.
Définissons quelques constantes. Les constantes sont créées avec le mot CONSTANT. On entre la valeur de la constante
ensuite le mot constant suivit de sont nom. Une fois une constante créée il suffit d'invoquer son nom pur que sa valeur soit déposée au
sommet de la pile des arguments.
Sans surprise les variables sont créées à l'aide du mot VARIABLE. Lorsqu'une variable est invoquée c'est son adresse qui est déposée
au sommet de la pile. Ce qui permet de lire sa valeur avec le mot @ (prononcé fetch) ou bien de modifier cette valeur avec le mot !
(prononcé store).
Il nous reste encore 2 variables à créer mais ces variables sont de type tableau (quoique je préfère le mot vecteur). Avant de créer ces 2 variables vecteurs
je vais définir un mot particulier appellé VECTOR. De la même manière que le mot VARIABLE nous a permit de créer des objets
de type variable, le mot VECTOR va nous permettre de créer des objets de type vecteur.
Dans ce tutoriel je vais appeller les mots qui servent à créer d'autres mots dans le dictionnaire mots compilants. CONSTANT VARIABLE, : et CREATE sont des mots compilants. Et maintenant que nous l'avons créé le mot VECTOR est aussi un mot compilant.
Le premier mot que nous avons utilisé pour définir VECTOR est le mot : Ce mot cré une nouvelle entrée dans le dictionnaire qui porte le nom qui le suis dans le texte d'entrée. En l'occurence ici il s'agit du mot VECTOR. Il existe une variable système qui s'appelle STATE. Cette variable détermine l'action de l'interpréteur. Lorsque cette variable est à 0 l'interpréteur exécute immédiatement chaque mot qu'il lit dans le texte d'entrée. Mais lorsque STATE a la valeur -1, l'interpréteur compile dans le dictionnaire l'adresse du code d'action au lieu de l'exécuter. Le mot : après avoir créer une entrée dans le dictionnaire pour VECTOR change la valeur de STATE à -1. Donc tous les mots qui suivent le mot VECTOR seront compilés dans le dictionnaire à la suite du mot VECTOR. Il y a une exception. Les mots dits immédiat sont quand même exécutés lorsque STATE est à -1. dans la définition de VECTOR on utilise 2 mots immédiats DOES> et ;. Le mot ; est utiliser pour terminer une définition, il sert simplement à remettre à 0 la valeur de STATE.
Le mot DOES> est plus complexe. Il termine la définition du mot VECTOR et compile une action qui va être exécutée par les mots créés avec
VECTOR. Autrement dit les mots créés par VECTOR, lorsqu'ils seront invoqués, vont exécuter le code:
Donc lorsque l'interpréteur à terminé la lecture du code ci-haut on a une nouvelle entrée dans le dictionnaire, VECTOR et on va s'en servir
pour créer des variables de type vecteur. Voici ce qui se passe lorsqu'on invoque le mot VECTOR. Regardons la partie de sa
définition qui précède DOES>.
Seulement 3 mots.
Nous allons maintenant créer 2 variables de type vecteur. Le mot vector consomme 1 argument qui est la taille du vecteur, c'est
à dire le nombre de cellules qui faut réservés dans L'espace de données pour son usage. Le mot vector lit le mot qui le suis dans le
texte d'entrée et l'utilise comme nom du vecteur nouvellement créé.
Prenons l'exemple de C-HEAD. On empile la valeur 4 qui sera consommé par VECTOR et qui est la taille de C-HEAD.
Une fois que l'interpréteur a lu cette phrase il y a donc un nouveau mot dans le dictionnaire appellé C-HEAD.
L'initialisation de C-HEAD nous montre comment utiliser un vecteur. On accède aux éléments d'un vecteur en indiquant
l'indice de l'élément suivit du nom du vecteur. Comme en C les indices commencent à zéro. L'adresse de l'élément est
calculée à partir de cet indice et est laissé au sommet de la pile. Prenons en exemple la phrase suivante:
Cette phrase comprend 4 mots, voyons ce qui se trouve sur la pile des arguments après l'exécution de chacun d'eux.
mot | description | État de la pile |
---|---|---|
'<' | Il s'agit d'un entier de type caractère dont la valeur ASCII est 60. | 60 |
east | Il s'agit d'une constante ça valeur est empilée. | 0 60 |
c-head | Il s'agit d'un vecteur. Il consomme la valeur au sommet de la pile et le remplace par l'adresse de cet élément. |
a-addr 60 |
! | Store consomme 2 valeurs sur la pile. Le premier est une adresse et le deuxième élément est la valeur à déposer à cette adresse. | pile vide maintenant c-head[0]=60 |
Comment est calculé l'adresse de l'élément du vecteur? C'est justement la partie de la définition de VECTOR qui suis DOES>
qui fait ce travail. Lorsque le mot C-HEAD est invoqué l'adresse de son champ de données est déposé au sommet de la pile ensuite le
code suivant est exécuté.
mot | description | État de la pile |
---|---|---|
Avant l'invocation de c-head on sur la pile | 0 60 | |
c-head | Lorsque c-head est invoqué mais avant que le code DOES> ne soit exécuté on a sur la pile. | pfa 0 60 |
swap | Commute les 2 éléments au sommet de la pile. | 0 pfa 60 |
cells | Multiplie l'indice au sommet de la pile par 2. | 2*0 pfa 60 |
+ | Additionne les 2 valeurs au sommet de la pile. a-addr=0+pfa | a-addr 60 |
Notez ici combien il est important lorsqu'on programme en Forth de connaître ce qu'il y a sur la pile des arguments ou même des retours si on
utilise cette dernière pour conserver des valeurs temporairement. On peut s'aider à l'aide de commentaires qui indique les valeurs sur
la pile des arguments par une liste précédé de S: et R: pour la pile des retours. Voici un exemple de commentaire.
Pour comprendre cet exemple il faut savoir qu'on nomme le sommet de la pile des arguments T et que le mot >R
transfert le sommet de la pile des arguments vers la pile des retours et que
le mot R> fait exactement le contraire. Tandis que le mot DROP jette la valeur au sommet de la pile des arguments.
On va maintenant définir quelques mots qui seront utilisés dans l'affichage du jeu. Chaque définition d'un mot commence
par le mot : et se termine par le mot ;.
Pour sauver de l'espace de données le jeu snake conserve le couple de coordonnées {x,y} en 1 seul entier plutôt que 2. Il nous
faut donc une fonction pour convertir le couple {x,y} en entier unique u. Tout a long de ce tutoriel je vais
référé ce type donnée sous le nom ucoord.
Cette conversion est simple puisqu'il s'agit de multiplier y par 256 et ensuite d'additionner
x au produit. Pour que ça fonctionne x et y doivent-être dans le domaine {0..255}.
Le mot ucoord>xy est l'inverse du précédent. Il convertie un entier non signé ucoord en 2 entiers représentant
un couple de coordonnées {x,y}. Il suffit de diviser l'entier par 256 et de conserver le quotient et le reste. y
est le quotient et x le reste. Le mot /mod divise un entier par un autre et conserve le quotient et le reste.
Le quotient est au sommet de la pile des arguments et le reste en second. On se retrouve donc avec le couple {x,y} dans le bon
ordre sur la pile des arguments.
Le mot draw-pixel en fait affiche un caractère c à la position de l'écran déterminé par le couple de coordonnées {x,y}.
On doit d'abord ajuster les coordonnées à celle de l'écran du moniteur. Selon les coordonnées de l'écran du moniteur le coin supérieur gauche
de l'aire de jeu est à la position {2,2} donc on ajoute y-offset dont la valeur est 2 et x-offset dont la valeur est aussi 2 à chacune
des coordonnées. Le mot swap est utilisé pour commuter la position des 2 éléments qui sont au sommet de la pile. Pour travailler sur
x on le met au sommet avec le premier swap et une fois x ajusté on remet les coordonnées dans le bon ordre avec le second
swap. Le mot at-xy sert simplement à déplacer le curseur texte à la position désignée par {x,y}. Le mot emit lui sert à
afficher le caractère c à cette position de l'écran.
Le mot draw-ring sert à dessiner une pastille en inverse vidéo. Ces pastilles représentent à la fois la nourriture et les anneaux qui
construisent le corps du serpent. Le mot b/w consomme un argument booléen et met le mode vidéo en noir sur fond blanc lorsque cette valeur
est vrai ou en vidéo normale si elle est fausse. b/w signifit Black on White. Puisqu'on dessine les anneaux en inverse vidéo
on fait donc true b/w. draw-ring consomme un argument u qui est l'ucoord de la position. On convertie cette valeur
en couple{x,y} avant d'appeller draw-pixel. Finalement on retourne au mode vidéo normal avec false b/w.
Le mot draw-walls dessine la bordure qui délimite l'aire du jeu. le mot cls vide l'écran de la console. whiteln dessine 1
ligne blanche à la position y qui lui est fournie en argument. Ici 2 lignes blanches sont dessinées en haut ( ligne 1) et en bas (ligne 24) de l'écran.
Pour dessiner les lignes verticales à droite et à gauche de l'écran on utilise une boucle avec compteur DO ... LOOP. Entre le mot
do et le mot loop se sont les instructions qui s'exécutent en bloucle.Le mot DO consomme 2 arguments, 24 est la
limite du compteur et 2 est la valeur initiale du compteur. On a déjà vu le mot at-xy qui consomme 2 arguments, soit les coordonnées
colonne et ligne où doit-être positionner le curseur texte. le mot i sert à empiler la valeur du compteur de boucle. Les instructions de la
boucle commencent donc par empiler la valeur 1 ensuite la valeur du compteur de boucle i, dont la valeur est 2 à la première itération.
On positionne le curseur et on affiche un caractère espace en invoquant le mot space. Comme on est en inverse vidéo cet espace apparaît
comme un carré blanc. On recommence l'opération à la colonne 64 et on arrive à loop qui incrémente le compteur pour ensuite le
comparer à la limite. Si cette limite est atteinte l'exécution continue après le loop sinon on retourne après le do.
Donc ici puisque do a été initialisé avec une limite de 24 cette boucle imprime un espace aux colonnnes 1 et 64 à partir de la
ligne 2 jusqu'à la ligne 23. On a donc 2 lignes verticales de chaque côté de l'écran.
Le mot draw-snake sert à dessiner le serpent. On commence par dessiner la tête. Comme on l'a dit le caractère utilisé pour la tête dépend de la
direction du mouvement du serpent. Cette direction est indiquée par la valeur de la variable HEAD. On lit donc cette valeur avec la phrase
head @ et cette valeur sert d'indice pour aller chercher le bon caractère dans le vecteur c-head par la phrase c-head @. Ensuite on
obtient la position de la tête du serpent avec la phrase 0 snake @ et on dessine la tête avec la phrase ucoord>xy draw-pixel. Pour dessiner
le corps du serpent on utilise encore une boucle do...loop. Cette boucle est initialisée avec la longueur du serpent qu'on obtient avec la phrase
snake-len @ et l'indice de départ est 1 puisqu'on a déjà dessiner la tête. la phrase i snake @ nous donne la coordonnée de l'ième
élément du serpent et draw-ring dessine l'anneau. Et bien sur loop incrémente le compteur et nous renvoie au début de la boucle tant qu'on
a pas atteint la limite.
draw-food sert à dessiner la pastille de nourriture. L'ucoord de cette pastille est obtenue par la lecture de la variable food avec
la phrase food @. Il suffit ensuite d'appeller draw-ring.
Le mot move-snake, qui sert à déplacer le serpent, est plus complexe que les précédents. Je vais donc commencer par expliquer comment on déplace le
serpent avant d'examiner le code qui effectue cette action.
head | modification |
---|---|
est | x=x+1 |
sud | y=y+1 |
ouest | x=(x-1)&255 |
nord | y=y-1 |
On commence donc par lire la valeur ucoord de la tête du serpent avec la phrase 0 snake @. dup fait une copie de cette valeur car nous aurons besoin de cette valeur pour l'affecter au premier anneau du serpent. On transforme l'entier en couple x,y avec ucoord>xy.
Voici comment fonctione le CASE ... ENDCASE en Forth. Le CASE sert à sélectionner un choix multiple en comparant la valeur au sommet de la pile avec différentes valeurs de contrôles. Ici la valeur au sommet de la pile est la direction obtenue par head @. Après le case suis la liste des valeurs de contrôle. Dans ce cas ci, il s'agit des constantes east, south, west et north. Le mot of suis chaque valeur de contrôle et est lui-même suivit du code à exécuter si la valeur de contrôle est égale à la valeur au sommet de la pile. endof indique la fin du code pour cette condition. Il y a autant de x of ... endof qu'il y a de valeurs à contrôler. Le mot endcase termine le case en retirant du sommet de la pile la valeur utilisée par case. Si aucune condition n'est rencontrée on peut insérer entre le dernier endof et endcase du code qui sera exécuté par défaut. Attention, si le code par défaut laisse une valeur sur la pile il doit faire un swap pour ramener la valeur utilisée par case au sommet pour que celle-ci soit jetée par endcase.
Maintenant que les coordonnées de la tête ont étées modifiées à l'intérieur du case on reconvertie le couple x,y en ucoord avant de sauvegarder cette nouvelle valeur dans l'élément 0 du vecteur snake par la prhase xy>ucoord 0 snake !.
A ce point ci il nous reste au sommet de la pile l'ancienne ucoord de la tête. On commence une boucle pour déplacer le reste du serpent.
snake-len @ 1 do. Le code à l'intérieur du do...loop est en fait simple à comprendre. Pour chaque élément i du vecteur snake,
on commence par lire la valeur actuelle, on commute les 2 éléments au sommet de la pile et on sauvegarde la valeur qui est maintenant au sommet dans
cet ième élément. dup >r sert simplement à conserver une copie de l'adresse du ième élément sur la pile des retours pour ensuite récupérer cette
adresse avec r> lorsque viens le temps d'enregistrer la nouvelle valeur ucoord de cet élément. Gardez en tête qu'au début de chaque boucle
la valeur qui est au sommet de la pile des arguments est la position avant déplacement de l'élément précédent du serpent. Action à l'intérieur de la boucle:
i snake dup >r @ swap r> !
action | S: | R: | commentaire |
---|---|---|---|
ucoord | juste avant de débuter le do...loop on a sur la pile l'ancienne position de la tête. | ||
i | compteur-boucle ucoord | Le compteur de boucle débute à 1 | |
snake | a-addr(i) ucoord | Maintenant au sommet de la pile on a l'adresse du ième élément du vecteur snake. | |
dup | a-addr(i) a-addr(i) ucoord | On a créer une copie de l'adresse. | |
>r | a-addr(i) ucoord | a-addr(i) | On envoie la copie sur la pile des retours. |
@ | ucoord-i ucoord | a-addr(i) | Fetch lit la valeur à L'adresse a-addr(i) Maintenant on a au sommet de S: ucoord du ième élément. |
swap | ucoord ucoord-i | a-addr(i) | On a commuter les 2 éléments au sommet de S: |
r> | a-addr(i) ucoord ucoord-i | On a ramené l'adresse du ième élément sur S: | |
! | ucoord-i | Store la valeur de l'élément (i-1) dans l'élément (i) et conserve l'ancienne valeur de l'élément (i) au sommet de S: |
Pourquoi conserve-t'on l'ancienne position du dernier à anneau du serpent dans tail? Parce qu'au moment où on déplace le serpent on ne sais pas encore si le serpent a avalé la pastille de nourriture. Lorsqu'on va faire cette vérification et dans l'éventualité ou on doit rallonger le serpent on va dessiner le nouvel anneau à cette position.
Le mot status sert à afficher l'état du jeu, soit le pointage et la longueur du serpent. Cette affichage est présenté sur la première ligne de l'écran en inverse vidéo, le mot commence donc par la phrase true b/w puis la phrase 1 1 at-xy déplace le curseur dans le coin supérieur gauche de l'écran. La phrase ." SCORE:" affiche le texte entre guillemets. On obtient le pointage par la lecture de la variable score et on imprime cette valeur à la position courante du curseur texte avec le mot . En Forth le mot . (point) sert à imprimer l'entier qui se trouve au sommet de la pile. Ensuite on déplace le curseur texte à la colonne 16, ligne 1 avec la phrase 16 1 at-xy et imprime le texte LENGHT: avec la phrase ." LENGTH:". Finalement on affiche la longueur du serpent avec la phrase snake-len @ .. Avant de quitter on repasse en mode vidéo normal false b/w.
On va commencer à examiner les mots qui construisent la logique du jeu.
Le mot valid-food? vérifie la position de la pastille par rapport au corpd du serpent.
Lorsque la pastille de nourriture a été gobée par le serpent une nouvelle pastille doit-être créé. La position de cette pastille est déterminée au
hasard donc on doit s'assurer qu'elle n'est pas sur le serpent. valid-food? compare donc la ucoord de la pastille nouvellement générée
avec la position de chacun des anneaux du serpent. S'il y a coïncidence valid-food? retourne la valeur FALSE. valid-food? consomme
donc en argument l'ucoord de la pastille et retourne une valeur booléene f. On commence par déposer sur la pile la constante TRUE
en la glissant en 2ième position avec swap. Ensuite on initialise une boucle avec compteur snake-len @ 0 do. i snake @ over = effectue
la comparaison entre l'argument u et l'ucoord de l'ième anneau du serpent. C'est la première fois qu'on rencontre le mot over dans
ce tutoriel. over fait une copie du 2ième élément de la pile et le dépose au sommet donc après l'exécution de over on a sur la pile des
arguments:
S: |
---|
u |
ucoord(i) |
u |
TRUE |
S: |
---|
f |
u |
TRUE |
mot exécuté | S: |
---|---|
swap | TRUE u |
drop | u |
false | FALSE u |
swap | u FALSE |
leave | u FALSE |
Le mot new-food introduit une nouvelle structure de contrôle, le begin ... until. Cette boucle se répète tant que la valeur laissée au sommet de la pile par la liste d'instructions entre le begin et le until est 0. On génère au hasard avec la fonction rand une valeur x et ensuite une valeur y. La phrase rand abs play-width mod est conçue pour limiter la valeur générée entre 0 et play-width-1. rand génère un entier quelconque entre -32768 et 32767, abs retourne la valeur absolue de cet entier. play-width empile la valeur 62 et mod retourne le reste de la division de l'entier par 62. La même opération est répétée pour générer la coordonnée y. xy>ucoord convertit le couple {x,y} en ucoord, crée une copie avec dup et appel valid-food? pour faire la vérification dont on a parlé au paragraphe précédent. Si valid-food? retourne TRUE on quitte la boucle begin ... until et on sauvegarde u dans food. Par contre si valid-food? retourne FALSE la boucle se répète mais avant de générer une nouvelle valeur de 'u' on doit jeter celle qui n'est pas bonne, c'est pourquoi la boucle commence avec un drop et qu'avant de commencer la boucle on avait mit sur la pile un 0 inutile.
On doit vérifier les collisions. Il y a 2 types de collisions, le serpent avec un mur et le serpent avec lui-même.
Le mot snake-bite? Sert à vérifier que le serpent n'est pas entré en collision avec lui-même. Il s'agit simplement de comparer l'ucoord de la
tête du serpent avec chacun des anneaux du serpent. S'il y a égalité entre ucoord(0) et ucoord(i) c'est que le serpent se mord lui-même. Encore une
fois on utilise une boucle avec compteur do ... loop. Mais avant d'entrer dans cette boucle on fait 2 choses. Premièrement on empile la constante
FALSE et ensuite la phrase 0 snake @ empile l'ucoord(0). Lorsqu'on entre dans la boucle on a donc 2 valeurs au sommet de la pile des
arguments:
S: |
---|
ucoord(0) |
FALSE |
mot exécuté | S: |
---|---|
0 | 0 |
snake | a-addr(0) |
@ | ucoord(0) |
ucoord>xy | y x |
play-height | 22 y x |
1- | 21 y x |
u> | f x |
swap | x f |
play-width | 62 x f |
1- | 61 x f |
U> | f f |
or | f |
Je vais en profiter ici pour faire la distinction entre les mots qui opèrent sur des entiers et commencent par la lettre u et les autres. Ceux qui
commecent par un u considèrent les entiers comme non signés. Voici un exemple:
-3 3 > \ retourne FALSE
-3 3 u> \ retourne TRUE
Parce que Forth utilise l'arithmétique en complément de 2, les nombres négatifs sont représentés par tous les entiers dont le bit le plus significatif est à
1. La représentation binaire de -3 est 1111111111111101 or cette série de bits, si on la considère comme un nombre non signé, donne la valeur
65533 ce qui est évidemment plus grand que 3. Vous pouvez le vérifier simplement en entrant sur la ligne de comamnde la phrase suivante:
-3 u.. Le mot u. imprime l'entier au sommet de la pile comme le mot . sauf qu'il considère ce nombre comme étant non signé.
Donc lorsqu'on veut vérifier s'il y a eu collision, quoi de plus logique que de définir le mot collision? qui va retourner vrai
si l'un ou l'autre de ces événement c'est produit. Le mot or signifit ou et retourne vrai si l'un ou l'autre des indicateurs
booléen au sommet de la pile est vrai.
On a décidé dans les règles du jeu que la valeur d'une pastille dépendait de sa position, coins 4 points, le long d'un mur 2 points et 1 point
pour les autres. Il nous faut donc vérifier cette position.
Le mot in-corner? vérifie si la pastille est dans un coin. Ce mot consomme l'argument u1 qui est l'ucoord de la pastille
de nourriture. On commence par convertir l'ucoord en couple {x,y}. Si la coordonnée x et la coordonnée y sont toutes les
2 le long d'un mur alors la pastille est forcément dans un coin. Puisque la coordonnée y est au sommet on va vérifier celle-ci en premier.
Puisqu'on a 2 vérifications à faire, mur du haut et mur du bas, on fait une copie de y. Pour le mur du haut on compare y à 0 avec
le mot 0=. On renvoie cet indicateur en 2ième position sur la pile avec swap. Maintenant y qui est à nouveau au sommet
est comparé avec play-heigth-1 qui est la position le long du mur du bas. On a maintenant 2 indicateurs booléens au sommet de la pile.
Il suffit de faire un or entre les 2. On fait un swap pour ramener la coordonnée x au sommet de la pile et on recommence
la même opération. Après le deuxième or on a encore 2 indicateurs booléens au sommet de la pile. Ces 2 indicateurs doivent-être vrai
pour indiquer que la pastille est dans un coin on les combine donc avec un and.
mot exécuté | S: |
---|---|
ucoord>xy | y x |
dup | y y x |
0= | f y x |
swap | y f x |
play-height | 22 y f x |
1- | 21 y f x |
= | f f x |
or | f x |
swap | x f |
dup | x x f |
0= | f x f |
swap | x f f |
play-width | 62 x f f |
1- | 61 x f f |
= | f f f |
or | f f |
and | f |
C'est le bon moment pour introduire la notion de factorisation. Si vous regardez la définition des mots in-corner? et on-wall?
elles sont identique sauf pour le dernier mot qui est and dans le premier cas et or dans le deuxième. Nous allons éliminer ce dédoublement
en créant un nouveau mot qui exécute le code commun au 2 définitions et redéfinir in-corner? ainsi que on-wall?. Notre nouveau mot est
borders?
L'indicateur au sommet de la pile est VRAI si la coordonnée x est le long d'un mur vertical. Le second indicateur est VRAI si la
coordonnée y est le long d'un mur horizontal. Si les 2 coordonnées sont le long d'un mur la pastille est forcément dans un coin on redéfinie donc
in-corner?
Pour que la condition on-wall? soit vrai il suffit que x ou y soit le long d'un mur, donc:
Ce procédé de mise en commun s'appelle factorisation et permet d'optimiser la taille du programme.
Maintenant on peut définir le mot score+ qui ajuste le pointage lorsque la pastille de nourriture est mangée par le serpent.
Ceci est la première version que j'ai écrite puis j'ai réalisé après avoir factorisé in-corner? et on-wall? que je pouvais réécrire
score+ et du même coup éliminer les mots in-corner? et on-wall?. Voici la nouvelle version de score+.
On commence par accorder un point puis pour chaque indicateur vrai retourné par borders? on multiplie le gain par 2. après le dernier then
on se retrouve avec le gain au sommet de la pile, il suffit de l'ajouté au pointage avec la phrase score +!. Finalement on met la valeur
-1 dans la variable food pour indiquer qu'il n'y a plus de pastille en jeu. La factorisation nous a fait sauver encore plus de
code que je l'envisageais au départ.
Lorsque le serpent avale une pastille en plus d'augmenter le pointage il faut aussi ajouter un anneau au serpent. Le mot snake+ accomplie cette
tâche.
snake-len est la variable qui contient la longueur du serpent. On doit incrémenter cette valeur de 1. On envoie une copie de l'adresse de
cette variable sur la pile des retours car on doit lire sa valeur et après l'avoir incrémentée il faut sauvegarder la nouvelle valeur. Bien sur on
aurait pu simplement invoqué snake-len 2 fois mais cette façon de faire montre comment on peut utiliser les mots >R et R>
pour sauvegarder temporairement une valeur sur la pile des retours pour accéder plus facilement les autres variables locales. Voici une autre version
de ce mot:
Ces deux définitions de snake+ font exactement la même chose mais en procédent différemment.
Rappellons-nous que la variable tail contient l'ucoord du dernier anneau du serpent avant l'exécution de move-snake.
Lorsqu'on rallonge le serpent le nouvel anneau occupe cette position.
Le mot eaten? Vérifie si la pastille a étée avalée par le serpent. Il suffit de comparer les ucoord de la variable food et
de la tête du serpent 0 snake @ pour l'égalité. En effet si en se déplaçant la tête du serpent atteint la même position que la pastille
de nourriture on considère celle-ci comme avalée.
On doit faire une lecture du clavier. C'est le rôle du mot game-exit?. Il s'appelle ainsi car le joueur peut choisir de quitter la partie
à n'importe quel moment en enfonçant la touche Q. game-exit? retourne TRUE si le joueur enfonce Q et FALSE
autrement. Autrement que la touche Q, game-exit? ne reconnait que les touches ← et → qui font pivoter la
tête du serpent vers la gauche ou la droite. On vérifie s'il y a une touche d'enfoncée avec ekey? et si c'est le cas on utilise ekey
pour obtenir la valeur de cette touche et ensuite une structure case ... endcase déjà étudiée pour décider de l'action à accomplir.
S'il n'y pas de touche enfoncée on retourne simplement FALSE. Notez que le q est accepté aussi bien que le Q.
Les deux retourne TRUE pour indiquer que le joueur veut quitter la partie. La valeur de head étant dans le domaine {0..3} on s'assure
qu'on demeure dans ce domaine avec la phrase 3 and après l'incrément ou le décrément. De cette façon le serpent tourne en rond après
4 pressions consécutives de la même touche: ← {0-3-2-1-0} ou → {0-1-2-3-0}.
Tous les mots nécessaires à l'exécution du jeu sont maintenant définis. La partie elle-même s'exécute à l'intérieur du mot game-loop.
La partie se déroule à l'intérieur d'une boucle begin ... until. On a déjà vu cette structure de contrôle. Cette boucle se répète tant que la
valeur au sommet de la pile lorsqu'on arrive au mot until est fausse. speed est une constante qui contrôle la vitesse du serpent.
Le mot ms consomme la constante laissé sur la pile par speed et pause l'exécution pour cette durée en millisecondes.
Au début de chaque boucle on affiche le status. Si l'ucoord de food est -1 ça signifit qu'il n'y pas de pastille de nourriture
dans l'aire de jeu donc on en cré une nouvelle avec new-food. On affiche la pastille avec draw-food. on appelle le mot game-exit?
qui fait une lecture du clavier et retourne un indicateur booléen. ?dup cré une copie de la valeur au sommet de la pile seulement si
cette valeur est différente de 0. Si cette valeur est nulle c'est que soit le joueur n'a pas enfoncé de
touche ou bien il a enfoncée une touche autre que Q. Dans ce cas on déplace le serpent move-snake, on vérifie s'il a mangé la
pastille en invoquant eaten?. S'il a mangé la pastille on augmente le pointage en invoquant score+ et rallonge le serpent snake+
autrement on vérifie s'il y eu collision?. Quel que soit le chemin parcouru on doit avoir au sommet de la pile un indicateur booléen
lorsqu'on arrive à until..
Mais avant d'exécuter une partie il faut initialiser les variables avec le mot game-init qui appelle snake-init.
srand initialise le générateur pseudo hasard (PRNG), c'est indispensable de le faire avant d'appeller rand, sinon ce dernier retourne
toujours 0. Ensuite on initialise snake-len à 4, score à 0 et food à -1. Ensuite on appelle
snake-init qui initialise head avec la constante east pour ensuite positionner le serpent au centre de l'écran.
Il ne nous reste que deux petits mots à définir, game-over? et snake-run. Le premier affiche un message pour demander au joueur
s'il veut vraiment quitter en enfonçant la touche Q ou bien jouer une autre partie en enfonçant n'importe qu'elle autre touche. game-over?
retourne un indicateur booléen VRAI si la touche Q a été enfoncée.
Le jeux est maintenant complété, il comprend 44 définitions. Ce nombre comprends tous les mots ajoutés au dictionnaire c'est à dire les constantes, les variables scalaires et vectorielles ainsi que les fonctions. Le texte source au complet comprend 172 lignes.
J'ai utilisé l'utilitaire BLKED pour créer ce jeu sur ForthEx. Le jeu est sauvegardé dans les blocs 1 à 10 de l'eeprom.
La vidéo suivante montre comment charger le jeu en mémoire RAM et le lancer. Pour ce démo j'utilise l'ordinateur en remote console et vokoscreen pour
enregistrer la session. Le logiciel utilisé pour la console remote est minicom.
Si vous avez bien assimilé ce tutoriel vous comprenez suffisamment le langage ForthEx qui pour l'essentiel se conforme au standard ANSI FORTH. Il ne vous reste qu'à élargir votre vocabulaire en lisant le reste de la documentation et à pratiquer. Vous pouvez pratiquer le forth en téléchargeant gratuitement swift forth qui fonctionne tout aussi bien sous Windows, Linux et OSX.