Projet : jeu Snake

Principe

Objectif

L'objectif de votre projet est de créer un clone du jeu Snake (comme celui-ci).

Vous le rendrez sous la forme d'un dépot GIT dont vous fournirez l'URL.

Avertissement

Il existe de nombreux tutoriels en ligne pour créer un jeu de Snake en Javascript.

Il n'est pas interdit de vous en inspirer, mais on attend de vous que vous écriviez votre propre code, pas que vous recopiiez passivement le code du tutoriel.

Notamment, votre note dépendra largement du respect des recommandations ci-dessous (les tutoriels optent généralement pour des solutions différentes).

Votre application comportera au minimum deux "écrans" :

  • un écran d'accueil, présentant le jeu et permettant de choisir parmi différents niveaux (cf. ci-dessous),

  • un écran de jeu, où se déroule le jeu à proprement parler, en l'affichant dans un canvas 2D, et en affichant le score du joueur.

Le passage d'un écran à l'autre doit se faire sans changer d'URL (vous devez donc modifier la page par manipulation du DOM, et non en chargeant une nouvelle page HTML).

Représentation interne

L'état du monde est représenté dans un tableau JS à deux dimensions. Chaque cellule du tableau représente une "case" du monde, et indique (par sa valeur) ce ce que contient cette case (mur, serpent, nourriture...). Il vous est recommandé de définir des constantes correspondant à chaque type d'objet, pour améliorer la lisibilité du programme.

_images/snake-grid.svg

Exemple de grille.

// La représentation qu'aurait cette grille en Javascript
let WORLD = [
  [EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY],
  [EMPTY, EMPTY, EMPTY, FOOD,  EMPTY, EMPTY],
  [EMPTY, SNAKE, EMPTY, EMPTY, EMPTY, EMPTY],
  [EMPTY, SNAKE, EMPTY, EMPTY, EMPTY, EMPTY],
  [EMPTY, SNAKE, SNAKE, EMPTY, EMPTY, EMPTY],
  [EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY],
];

Par ailleurs, l'état du serpent est également représenté par un tableau, dont chaque cellule contient les coordonnées d'une case du serpent. Le début de ce tableau correspond à la queue du serpent, et la fin du tableau correspond à sa tête. Lorsque le serpent se déplace, on ajoutera la nouvelle position de sa tête à la fin (avec la méthode push) et on retirera l'ancienne position de sa queue (avec la méthode shift).

// La représentation qu'aurait le serpent dans la figure ci-dessus.
let SNAKE_BODY = [[4,2], [4,1], [3,1], [2,1]];

L'information sur le serpent est donc stockée de manière redondante dans ces deux tableaux. Cette redondance rendra la gestion du jeu plus simple par la suite.

Contrôles

L'événement keydown est produit chaque fois que l'utilisateur enfonce une touche.

L'objet événement passé en paramètre du listener possède un attribut key, qui représente la touche enfoncée. Lorsque la touche correspond à un caractère imprimable, key contient ce caractère. Sinon, il contient le nom de la touche, comme décrit dans ces tableaux. En particulier, les flèches sont représentées respectivement par les chaînes "ArrowDown", "ArrowLeft", "ArrowRight" et "ArrowUp".

Chaque fois qu'une touche pertinente pour le jeu est enfoncée, vous mémoriserez dans une variable la valeur de l'attribut key. Cette valeur sera utilisée dans la fonction step (cf. ci-dessous).

Déroulement d'une partie

Toute la mécanique du jeu sera gérée dans une fonction nommée step, qui sera appelée à intervalle régulier, grâce à la fonction setInterval.

Dans cette fonction, vous devrez effectuer les étapes suivantes.

  1. Vérifier si l'utilisateur a enfoncé une touche, et modifier la direction du serpent en conséquence (si cela est compatible avec sa direction actuelle).

  2. Calculer la nouvelle position de la tête du serpent en fonction de sa direction.

  1. Vérifier si la tête du serpent rencontre de la nourriture, un mur, ou un morceau de son corps.

    • Dans le premier cas, le score augmente, et une autre nourriture est ajoutée dans une case vide aléatoire.

    • Dans les autres cas, la partie se termine.

  2. Mettre à jour le tableau SNAKE_BODY en faisant avancer le serpent ; s'il a mangé de la nourriture, son corps doit s'allonger (ce qui revient à ne pas réduire sa queue). Mettre également à jour le tableau WORLD en conséquence.

  3. Effacer intégralement le canvas, et re-dessiner l'état de WORLD. (On pourrait envisager de ne redessiner que les parties qui ont changées, mais cette méthode est plus simple et plus évolutive).

Gestion des niveaux

Chaque niveau est décrit par un fichier JSON ayant la structure suivante:

 1{
 2    "dimensions": [80, 40],
 3    "delay": 200,
 4    "walls": [
 5        [5,5], [5,6], [5,7], [5,8], [70, 35], [71, 35], [72, 35]
 6    ],
 7    "food": [
 8        [10,10]
 9    ],
10    "snake": [
11      [60,30],
12      [60,29],
13      [60,28]
14    ]
15
16}

  • dimensions donne la taille de la grille dans laquelle évolue le serpent,

  • delay donne le délais (en ms) entre deux appels à la fonction step,

  • walls donne la position des cases de mur,

  • food donne la position initiale de la nourriture

  • snake donne la position initiale du serpent

La page de garde doit donner le choix entre plusieurs niveaux ; lorsque le joueur en choisit un, le fichier JSON correspondant est chargé, et les variables WORLD et SNAKE_BODY sont initialisées conformément.

Extensions

Le cahier des charges ci-dessus est minimaliste. À vous de faire preuve d'originalité en l'étendant. Par exemple :

  • Complexification du jeu (en étendant le format JSON de description des niveaux) :
    • faire accélérer le serpent progressivement (selon les niveaux);

    • faire disparaître la nourriture automatiquement au bout d'un certain temps (pour la faire réapparaître ailleurs) ;

    • ajouter d'autres types de terrain (par exemple, la glace, qui empêche de changer de direction) ;

    • offrir plusieurs types de nourriture, certains offrant des capacités spéciales (diminuer la taille du serpent au lieu de l'augmenter, creuser sous terre...) ;

    • offrir un mode multi-joueur (sur le même poste)...

  • Améliorations esthétiques :

    • intégrer des sons ;

    • utiliser des images bitmaps plutôt que des carrés monochromes ;

    • utiliser un canvas WebGL pour représenter le jeu...