Présentation

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.

retour à l'index principal

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.

règles du jeu

  1. Le serpent ne doit pas entrer en collision avec la bordure de l'aire de jeu.
  2. Le serpent ne doit pas se mordre lui-même.
  3. Le serpent mange les pastilles qui apparaîssent dans l'aire de jeux.
  4. Le pointage est compté de la façon suivante:
    • Une pastille dans un coin compte pour 4 points.
    • Une pastille le long d'un mur compte pour 2 points.
    • Les autres pastilles comptent pour 1 point.
  5. Lorsque le serpent mange une pastille il s'allonge d'un anneau.
  6. Le serpent est toujours en mouvement mais sa direction est contrôlée par les touches et .

L'aire de jeu

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.

Analyse du programme.

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.

Variables

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.

Déroulement du progamme (algorithme)

  1. Déclaration des constantes.
  2. Déclaration des variables.
  3. Initialisation d'une partie:
    • Initialisation des variables.
    • Création du serpent initial.
    • Dessin des murs.
  4. Boucle de la partie:
    1. Afficher le status.
    2. S'il n'y a pas de pastille dans l'aire de jeux en créer une.
    3. Afficher la pastille.
    4. déplacer le serpent d'un pas.
    5. Dessiner le serpent à sa nouvelle position.
    6. Si le serpent a avalé la pastille ajuster le pointage et la longueur.
    7. Sinon vérification collision, si collision terminer partie.
    8. Délais vitesse déplacement.
    9. Lire clavier
      Si touche ou enfoncée, modifier variable HEAD.
      Si touche Q enfoncée quitter la boucle de jeu.
    10. Retourné à l'étape 4.1.
  5. Signaler fin de la partie.
  6. Attendre touche pour intialiser la prochaine partie ou (Q)uitter.

Voici l'apparance du jeu au démarrage d'une partie. Au centre c'est le serpent qui se dirige vers l'est.

La machine virtuelle Forth

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.

Pile des arguments

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.

Pile des retours

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.

Cellule

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.

Le dictionnaire

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.

Constantes et variables système

La machine virtuelle ForthEx utilisent un certain nombre de constantes et variables qui sont accessible au programmeur nous les mentionnerons lors de leur utilisation.

La syntaxe Forth

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.

Les commentaires

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.

Les indentifiants

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.

La notation préfixée.

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.

Débutons le codage

À 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.
motdescriptionÉtat de la pile
'<'Il s'agit d'un entier de type caractère dont la valeur ASCII est 60.60
eastIl s'agit d'une constante ça valeur est empilée.0
60
c-headIl 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é.
motdescriptionÉtat de la pile
Avant l'invocation de c-head on sur la pile0
60
c-headLorsque c-head est invoqué mais avant que le code DOES>
ne soit exécuté on a sur la pile.
pfa
0
60
swapCommute les 2 éléments au sommet de la pile.0
pfa
60
cellsMultiplie 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+pfaa-addr
60

Là on voulait accéder l'élément 0 du vecteur donc l'adresse n'a pas changée mais pour les autre éléments l'adresse est incrémentée de 2*ii est l'indice qu'on veut accéder.

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.

Premiers mots du jeu

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.

  1. La variable head nous indique dans qu'elle direction on doit déplacer le serpent. Selon la valeur de head on doit:
    headmodification
    estx=x+1
    sudy=y+1
    ouestx=(x-1)&255
    nordy=y-1
  2. Le corps du serpent suis la tête çe qui signifit qu'une fois que la tête a été déplacée le premier anneau va à la position précédente de la tête. Le deuxième anneau à la position précédente du premier anneau, ainsi de suite jusqu'à la queue du serpent.
  3. Il s'agit donc de déplacer la tête en premier pour ensuite utiliser une boucle do...loop pour déplacer le reste du serpent.

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.

CASE

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> !
actionS:R:commentaire
ucoordjuste avant de débuter le do...loop
on a sur la pile
l'ancienne position de la tête.
icompteur-boucle
ucoord
Le compteur de boucle débute à 1
snakea-addr(i)
ucoord
Maintenant au sommet de la pile
on a l'adresse du ième élément du vecteur snake.
dupa-addr(i)
a-addr(i)
ucoord
On a créer une copie de l'adresse.
>ra-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.
swapucoord
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-iStore 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:

Lorsqu'on quitte la boucle do...loop il reste au sommet de la pile l'ucoord de la queue du serpent avant déplacement. On doit faire 2 choses avec cette valeur. Premièrement on cré une copie et on la conserve dans la variable tail avec la phrase dup tail !. Ensuite on va effacer L'anneau qui se trouve à cette position puisque le serpent s'est déplacé il n'y a plus rien à cette coordonnée. bl swap ucoord>xy draw-pixel. Le mot bl empile le caractère espace. Finalement on appelle draw-snake pour redesssiner le serpent au complet à sa nouvelle position. Il y aurait moyen d'optimiser le redessinage du serpent puisque seul la tête et le premier anneau aurait besoin d'être redessiné. C'est laissé à titre d'exercice.

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.

La logique du jeu

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

Le mot = effectue une comparaison entre les 2 éléments au sommet de la pile et retourne TRUE s'il sont égaux. donc après l'exébution de = on a sur la pile:
S:
f
u
TRUE

Le mot if consomme la valeur au sommet de la pile et si cette valeur est vrai, (toute valeur différente de 0 est considérée comme vrai) les instructions situées entre le if et le then sont exécutées. Supposons que f est VRAI, alors la phrase suivante est exécutée: swap drop false swap leave:
mot
exécuté
S:
swapTRUE
u
dropu
falseFALSE
u
swapu
FALSE
leaveu
FALSE

Le mot leave sert à quitter prématurément une boucle do ... loop en faisant un saut juste après le loop. S'il y a superposition entre u et un ucoord(i) il n'est pas nécessaire de continuer. On remplace donc le TRUE qu'on avait mis en 2ième position sur la pile par un FALSE et on quitte. A la sortie de la boucle on jette la valeur u pour ne conserver que l'indicateur booléen.

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

L'intérieur de la boucle est très similaire à ce qui se passe dans valid-food?. i snake @ over = if swap drop true swap leave then. Pour chaque élément i on compare ucoord(0) et ucoord(i) pour l'égalité et en cas d'égalité en remplace la constante FALSE en deuxième position de la pile par la constante TRUE et on quitte la boucle avec LEAVE. A la sortie de la boucle on jette la valeur ucoord(0) qui restait sur la pile pour ne conserver que l'indicateur booléen vrai/faux.


Le mot wall-bang? vérifie l'autre type de collision, i.e. serpent et mur. Cette détection ne requiert aucune boucle. Il suffit simplement de s'assurer que les coordonnées x et y de la tête du serpent sont dans l'aire de jeu. On n'a qu'à lire l'ucoord(0) avec 0 snake @, convertir cette valeur en couple {x,y} et ensuite faire la vérifaction (x>(play-width-1))||(y>(play-height-1)) ce qui se traduit en Forth par: play-height 1- u> swap play-width 1- u> or. Notez l'astuce ici, on considère les coordonnées comme des entiers non signés ce qui évite d'avoir à vérifier que x ou y est plus petit que zéro. En effet si l'une où l'autre des coordonnées devient négative en considérant cette valeur comme non signée elle sera plus grande que 127.
mot
exécuté
S:
00
snakea-addr(0)
@ucoord(0)
ucoord>xyy
x
play-height22
y
x
1-21
y
x
u>f
x
swapx
f
play-width62
x
f
1-61
x
f
U>f
f
orf

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>xyy
x
dupy
y
x
0=f
y
x
swapy
f
x
play-height22
y
f
x
1-21
y
f
x
=f
f
x
orf
x
swapx
f
dupx
x
f
0=f
x
f
swapx
f
f
play-width62
x
f
f
1-61
x
f
f
=f
f
f
orf
f
andf


Le mot on-wall? fait la même vérification que le précédent mais dans ce cas si retourne vrai si x ou y est vrai.

Factorisation

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.

demo

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.

Conclusion

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.