1. Snake

Le snake, de l’anglais signifiant « serpent », est un genre de jeu vidéo dans lequel le joueur dirige une ligne qui grandit et constitue ainsi elle-même un obstacle. Bien que le concept tire son origine du jeu vidéo d’arcade Blockade développé et édité par Gremlin Industries en 1976, il n’existe pas de version standard. Son concept simple l’a amené à être porté sur l’ensemble des plates-formes de jeu existantes sous des noms de clone.

Le joueur contrôle une longue et fine ligne semblable à un serpent, qui doit slalomer entre les bords de l’écran et les obstacles qui parsèment le niveau. Pour gagner chacun des niveaux, le joueur doit faire manger à son serpent un certain nombre de pastilles similaire à de la nourriture, allongeant à chaque fois la taille du serpent. Alors que le serpent avance inexorablement, le joueur ne peut que lui indiquer une direction à suivre (en haut, en bas, à gauche, à droite) afin d’éviter que la tête du serpent ne touche les murs ou son propre corps, auquel cas il risque de mourir.

— wikipedia
https://fr.wikipedia.org/wiki/Snake_(genre_de_jeu_vid%C3%A9o)

2. Objectif

Notre objectif est développer une version Python de ce jeu en s’appuyant sur les concepts de programmation que nous avons appris jusqu’à maintenant :

  • Utilisation de modules

  • les fonctions

  • les listes

  • les tests conditionnels

  • les boucles

snake

3. Au travail…​

  • Cloner le dépôt depuis classroom.github : https://classroom.github.com/a/77g10ACT

  • Sur replit, créer un nouveau repl en important le code depuis github

  • Ne pas oublier de comit & push le code modifié après chaque étape du développement.

3.1. Le titre

Commençons par donner un titre à notre jeu, par exemple : PYTHON SNAKE. Petit clin d’oeil au jeu, au serpent (le python) et au langage avec lequel on le programme !

pour cela, je vous propose d’utiliser le concept de l'ASCII art

3.1.1. ASCII art

L’ASCII art consiste à réaliser des images uniquement à l’aide des lettres et caractères spéciaux contenus dans le code ASCII. Vous utilisez souvent cet art pour générer des émoticons. Nous allons ici pousser plus loin le concept pour générer une image du titre de notre jeu :

titre

Vous êtes libre de réaliser le design que vous souhaitez du moment qu’il utilise les caractères de la table ASCII

On trouve sur le net des sites de génération d’image ASCII art, par exemple : http://www.patorjk.com/software/taag

3.1.2. Codage du titre

titre =['  _______     _________ _    _  ____  _   _    _____ _   _          _  ________ ',
        ' |  __ \ \   / |__   __| |  | |/ __ \| \ | |  / ____| \ | |   /\   | |/ |  ____|',
        ' | |__) \ \_/ /   | |  | |__| | |  | |  \| | | (___ |  \| |  /  \  |   /| |__   ',
        ' |  ___/ \   /    | |  |  __  | |  | | . ` |  \___ \| . ` | / /\ \ |  < |  __|  ',
        ' | |      | |     | |  | |  | | |__| | |\  |  ____) | |\  |/ ____ \| . \| |____ ',
        ' |_|      |_|     |_|  |_|  |_|\____/|_| \_| |_____/|_| \_/_/    \_|_|\_|______|']
  • Ecrire une fonction affichage_titre(titre) qui parcours la liste titre et affiche chacune de ces lignes puis attend 2 secondes avant de passer à la suite (lancement du jeu):

def affichage_titre(titre):
        ...
        time.sleep(2)

Pour utiliser la fonction sleep() il faut importer le module time

import time

3.2. L’aire de jeu

3.2.1. Le module curses

Curses est le nom d’une bibliothèque logicielle permettant le développement sous Unix d’environnements plein écran en mode texte, indépendamment du terminal informatique utilisé, et pour tout type d’applications.

Elle est issue de la distribution BSD d’Unix, dans laquelle elle était employée pour l’éditeur de texte vi et le jeu Rogue.

Avec curses, il est possible d’écrire à tout moment en toute position de l’écran. Les programmeurs peuvent ainsi concevoir des applications basées sur le mode texte, sans tenir compte des particularités de chaque terminal. La bibliothèque curses s’occupe d’envoyer les caractères de contrôle appropriés vers le moniteur lors de l’exécution du programme, en essayant d’optimiser ce qu’elle peut quand elle le peut.

Rester en mode texte, mais en y utilisant ainsi l’adressage direct de tout endroit de l’écran, permet aussi de ne pas avoir à s’occuper du gestionnaire graphique utilisé parmi les nombreux que peut utiliser Linux, Unix ou Windows.

— wikipedia
https://fr.wikipedia.org/wiki/Curses

3.2.2. Utilisation

Utilisation basic de Curses

Les méthodes principales sont très simples :

  • initscr() initialise le mode plein écran.

  • move(ligne, colonne) déplace virtuellement le curseur

  • newwin(height, width, begin_y, begin_x) création d’une fenètre dans le terminal

  • addstr(chaîne) écrit une chaîne de caractères là où est le curseur

  • refresh() met en conformité l’écran réel et l’affichage virtuel

  • endwin() met fin au mode plein écran et restaure l’état où était l’écran avant que l’on ne s’y place.

La méthode newwin() permet de créer une fenètre qui est une abstraction de base de curses. Un objet fenêtre représente une zone rectangulaire de l’écran et prend en charge des méthodes pour afficher du texte, l’effacer, permettre à l’utilisateur de saisir des chaînes, etc.

Le détail de toutes les méthodes de curses : https://docs.python.org/fr/3/library/curses.html

Comment utiliser efficacement le module curses : https://docs.python.org/3/howto/curses.html

3.2.3. Codage de l’air de jeu

  • Ecrire une fonction affichage_aire_de_jeu(hauteur, largeur, titre) qui s’affichera en commençant par le bord supérieur gauche du terminal (point x=0, y=0)

fenetre vide
def affichage_aire_de_jeu(hauteur, largeur, titre):
    # Création d'une nouvelle fenètre en 0, 0
    win = curses.____
    # Les séquences d'échapement sont générés par certaines touches, les autres n'ont aucun effet
    win.keypad(__)
    # L'écho des caractères saisis est désactivé
    curses.noecho()
    # Pas de curseur visible
    curses.curs_set(__)
    # La saisie de caractère est non bloquante
    win.nodelay(1)
    # La fenètre a une bordure standard
    win.____
    # Définition d'une couleur pour le titre : texte en rouge sur fond blanc
    # Voir dans la documentation la table "lists the predefined colors"
    curses.init_pair(1, curses.____, curses.____)
    # Affichage du titre
    win.addstr(0, 27, titre, curses.color_pair(1))
    # Raffraichissement de la fenêtre
    win.____
    # Emission d'un beep
    curses.____
    # retourner la fenêtre
    return ____
  • Appeler cette fonction après l’appel de la fonction affichage_titre() et faire sorte qu’elle reste afficher à l’écran pendant 10 secondes.

# Affichge du titre pendant 2s avant l'ouverture de la fenêtre de jeu
affichage_titre(titre)
# Initialisation du terminal
curses._____
# Démarrage du mode couleur
curses.start_color()
# Affichage de l'aire de jeu
affichage_aire_de_jeu(20,60, 'SNAKE')
# Tempo 10s
curses.napms(____)
# fin de l'affichage de la fenêtre
curses.endwin()
  • Vérifier le bon fonctionnement

3.3. Contrôles du jeu

Le contôle du jeu est assuré par les touches de direction UP, DOWN, LEFT, RIGHT et Escape pour quitter la partie en cours.

3.3.1. Les définitions de touches de curses

Curses dispose de constantes qui permettent d’identifier certaines touches du clavier. Si les touches directionnelles sont bien présentes, ce n’est pas le cas de la touche Escape. Il nous faudra donc utiliser son code ASCII.

3.3.2. Codage des contrôles de jeu

  • Rechercher les constantes correspondantes au touches directionnelles (KEYS) dans la documentation : https://docs.python.org/fr/3/library/curses.html#constants

  • Rechercher dans la table ASCII la valeur décimal du code associé à la touche Escape (Echap)

  • Ecrire une fonction controle(win, key, keys = [ __ ]) ou keys est une liste des contrôles possibles. Renseigner la liste keys qui ici un argument par défaut de la fonction.

def controle(win, key, keys = [____]):
	'''
	Controles de jeu
	paramètres :
	  win : fenètre en cours
	  key : dernière touche reconnue
	  keys: liste des touches acceptées par défaut
	retour :
	  code de la touche reconnue
	'''
	# Sauvegarde de la dernière touche reconnue
	old_key = ____

	# Aquisition d'un nouveau caractère depuis le clavier
	key = win.____

	# Si aucune touche actionnée (pas de nouveau caractère)
	# ou pas dans la liste des touches acceptées
	# key prend la valeur de la dernière touche connue
	if key == ____ or key not in ____ :
		key = ____

	# Raffaichissement de la fenètre
	win.refresh()

	# retourne le code la touche
	return ____
  • Ecrire une fonction jeu(win) qui affichera le sepent et la pomme qu’il doit manger, assurera le comptage des point et l’arrêt du jeu par le joueur (Escape) ou lorqu’il perd.

def jeu(win):
	'''
	Moteur du jeu
	paramètre :
	  win : fenètre en cours
	retour :
	  score à la fin du jeu
	'''

	# initialisation du jeu
	# Le serpent se dirige vers la droite au début du jeu.
	# C'est comme si le joueur avait utilisé la flèche droite au clavier
	key = ____
	score = 0

	# Definition des coordonnées du serpent
	# Le serpent est une liste de d'anneaux composées de leurs coordonnées ligne, colonne
	# La tête du serpent est en 4,10, l'anneau 1 en 4,9, le 2 en 4,8
	snake = [[4, 10], [4, 9], [4, 8]]

	# La nouriture (pomme) se trouve en 10,20
	food = [10, 20]

	# Affichage la nouriture en vert sur fond noir dans la fenêtre
	curses.init_pair(2, curses.______, curses.______)
	win.addch(food[0], food[1], chr(211), curses.color_pair(2))  # Prints the food

	# Affichage du serpent en bleu sur fond jaune
	curses.init_pair(3, curses.____, curses.____)
	# sur toute la longeur du serpent
	for i in range(______)):
		# affichage de chaque anneau dans la fenêtre en ligne, colonne
		win.addstr(snake[i][0], snake[i][1], '*', curses.color_pair(3))

	# Emission d'un beep  au début du jeu
	curses.____

	# Tant que le joueur n'a pas quitter le jeu
	______________

		key = controle(win, key)

	return score
  • Appeler cette fonction après l’appel de la fonction affichage_aire_de_jeu() et faire afficher le score (ici de 0) lorsque l’utilisateur quitte le jeu. Le seul moyen de quitter (proprement) le jeu doit être l’utilisation de la touche Escape

affichage_titre(titre)
curses.initscr()
curses.start_color()
window = affichage_aire_de_jeu(20, 60, 'SNAKE')
score = jeu(window)
curses.endwin()

print('\n\n\n')
print(f'Votre score est de : {____}')
print('\n\n\n')
  • Vérifier le bon fonctionnement

3.4. Gérer les déplacements

3.4.1. Principe

Les déplacements sont obtenus en agissant sur les coordonnés du serpent. Lorsque le serpent avance dans une direction donnée, on insère un nouvel élément dans sa liste à la position 0 (la tête). Le serpent à donc grandi d’un anneau. Lorsque nous traiterons du cas ou il a manger une pomme, nous ne lui retirerons pas cet anneau mais pour le moment, nous traitons seulement le cas de l’avance, donc, il nous faut supprimer son dernier anneau pour conserver sa longueur :

Au départ

snake0

Le serpent se déplace vers la droite, on insère la tête …​

snake1

...et on supprime le dernier anneau

snake2

3.4.2. Codage des déplacements

  • Ecrire une fonction deplacement(win, score, key, snake, vitesse) qui gère les déplacents du serpent dans l’aire de jeu en fonction de la touche de contrôle active. Si le serpent atteint une limite de l’air de jeu, il réapparait sur le bord opposé :

def deplacement(win, score, key, snake, food):
	'''
	Déplacements du serpent
	paramètres :
	  win : fenètre en cours
	  score : score en cours
	  key : touche de controle en cours
	  snake : liste des positions en cours des anneaux du serpent
	  food : liste de la position de la pomme
	retourne :
	  tuple contenant la liste des positions en cours des anneaux du serpent et score en cours
	'''
	# Si on appui sur la flèche "à droite",
	# la tête se déplace de 1 caractère vers la droite (colonne + 1)
	if key == KEY_RIGHT:
		snake.insert(0, ________, ________)

	# Sinon si on appui sur la flèche "à gauche",
	# la tête se déplace de 1 caractère vers la gauche (colonne - 1)
	elif key == KEY_LEFT:
		snake.insert(0, ________, ________)

	# Sinon si on appui sur la flèche "en haut",
	# la tête se déplace de 1 caractère vers le haut (ligne - 1)
	elif key == KEY_UP:
		snake.insert(0, ________, ________)

	# Sinon si on appui sur la flèche "en bas",
	# la tête se déplace de 1 caractère vers le bas (ligne + 1)
	elif key == KEY_DOWN:
		snake.insert(0, ________, ________)

	# si la serpent arrive au bord de la fenêtre (20 lignes x 60 colonnes)
	if snake[0][0] == __:
		 ________________

	if snake[0][1] == __:
		________________

	if snake[0][0] == __:
		 ________________

	if snake[0][1] == __:
		________________


	# Suppression du dernier anneau du serpent.
	# Sera conditionner plus tard au fait que le serpent mange ou pas une pomme
	# Le score sera alors également mis à jour
	last = snake.pop()


	# Affichage de la tête à sa nouvelle position en bleu sur fond jaune
	win.addstr(____, ____, ___, curses.color_pair(__))

	# Effacement du dernier anneau : affichage du caractère "espace" sur fond noir
	win.addstr(last[0], last[1], ' ', curses.color_pair(1))

	# Affichage du score dans l'aire de jeu
	win.addstr(0, 2, 'Score : ' + str(score) + ' ')

	# Attendre avant le pas suivant
	vitesse = 1
	win.timeout(150//vitesse)

	# tuple contenant :
	# - la liste des positions en cours des anneaux du serpent
	# - score en cours
	return snake, score
  • Appeler cette fonction dans le cours du jeu, juste après avoir obtenu le contrôle souhaité :

def jeu(win):

	...

	while key != 27:

		key = controle(win, key)
		snake = deplacement(win, score, key, snake, food)

	return score
  • Vérifier le bon fonctionnement

3.5. On a mangé la pomme ?

On cherche ici à savoir si aucours du déplacement, le serpent a manger la pomme. Dans ce cas, les coordonnées de sa tête sont égales à celle de la pomme.

3.5.1. Principe

La position de la tête est donnée par l’élément 0 de la liste snake (snake[0]) et celle de la pomme par la liste food. Il suffit que ces deux listes soient égales pour que l’on en déduise que le serpent à manger la pomme !

3.5.2. Codage du test le serpent a manger la pomme

  • Ecrire une fonction mange_pomme(win, food, snake, score) qui teste si le serpent a mangé la pomme, met à jour le score et retourne les listes nécessaires à la mises à jour du serpent.

def mange_pomme(win, food, snake, score):
	'''
	Le serpent a-t-il mangé la pomme ?
	paramètres :
	  win : fenètre en cours
	  food : liste des coordonnées de la pomme
	  snake : liste des coordonnées des anneaux du serpent
	  score : score en cours
	retour :
	  Tuple constitué de :
	    - la liste des coordonnées actualisées de la pomme,
	    - la liste des coordonnées du serpent,
	    - la liste des coordonnées du dernier anneau à supprimer
	    - le score en cours
	'''
	# initialisation de la liste contenant les coordonnées du dernier anneau du serpent
	last = [0,0]

	# Si le serpent a mangé la pomme
	__________________:
		# Emettre un beep
		curses.beep()

		# incrémenter le score
		score _____________

		# Réactualiser les coordonnées de la pomme
		# On recommence tant que les coordonnées de la pomme sont dans le serpent
		while _____ in _____:

			# On actualise au hasard les coordonnées de la pomme
			# dans les limite de la fenêtre
			# voir la documentation de la fonction window.getmaxyx()
			food[0] = randint(1, ___________________)
			food[1] = randint(1, ___________________)

		# Affichage de la pomme aux nouvelles coordonnées en vert sur fond noir
		win.addch(____, ____, chr(211), curses.color_pair(2))
		win.refresh()

	# Sinon
	else:
		# Suppression du dernier anneau du serpent
		last = snake.pop()

	return food, snake, last, score
  • Modifier la fonction deplacement() pour qu’elle appelle la fonction mange_pomme() :

def deplacement(win, score, key, snake, food):

	...

	# Suppression du dernier anneau du serpent.
	# Sera conditionner plus tard au fait que le serpent mange ou pas une pomme
	# Le score sera alors également mis à jour
	# remplacer last = snake.pop() par :
	food, snake, last, score = mange_pomme(win, food, snake, score)

	...

	return snake, score
  • Vérifier le bon fonctionnement.

3.6. Quand est-ce qu’on perd ?

Si tout va bien pour le moment, on peut jouer mais pas perdre. En effet, nous n’avons prévu de cas perdant. Dans le scénario initial de Snake, le joueur perd si le serpent se mord la queue ou s’il entre en contact avec un bordure. Nous ne traiterons ici que du premier cas.

3.6.1. Principe

Lorsque le serpent se mord la queue, les coordonnées de sa tête sont identiques à celles d’une de ses cellules. La position de la tête est donnée par l’élément 0 de la liste snake (snake[0]) et celle du reste du corps la liste snake[1:]. Il suffit que ces deux listes soient égales pour que l’on en déduise que le serpent s’est mordu la queue !

3.6.2. Codage de la fin du jeu

def perdu(win, snake):
	'''
	Le serpent se mange-t-il la queue ?
	paramètre :
	  win : fenètre en cours
	  snake : liste des positions en cours des anneaux du serpent
	retourne :
	  True si on perd, False sinon
	'''

	# initialisation de la variable end à retourner
	end = ______

	# Si la tête du serpent est dans le corps
	if ______ in _____ :

		# Afiicher "GAME OVER !" en blanc sur fond rouge au milieu de la fenêtre
		curses.init_pair(4, curses.______, curses.______)
		win.addstr(________, ________, ________)
		win.refresh()

		# Emission d'une série de beep.
		# Vous devrez écrire vous-même cette fonction !
		beep_fin()

		# On laisse 2 secondes au joueur pour s'assurer
		# qu'il ait bien compris qu'il a perdu !
		curses.napms(2000)
		end = True
	return end
  • Modifier la fonction jeu() pour qu’elle appelle la fonction perdu() et mette fin au jeu si elle retourne True

def jeu(win):

	...

		end = False

	...

	# tant que pas Escape ou pas end
	while key != 27 ___  ___  end:

		key = controle(win, key)
		snake, score = deplacement(win, score, key, snake, food)
		end =  perdu(win, snake)

	return score
  • Vérifier le bon fonctionnement.

3.7. On complique un peu le jeu ?

Il y a plusieurs façon de compliquer le jeu. En modifiant la géométrie de l’aire de jeu (compliqué) ou plus simplement en accélérant le serpent en cours de jeu. C’est que nous allons faire ici.

3.7.1. Principe

Le changement de vitesse dans les déplacements du serpent est obtenu en agissant sur le temps entre deux raffraichissement de sa position. C’est l’appel à la fonction win.timeout(150//vitesse) ou le paramètre vitesse qui est actuellement fixé à 1. Il faut le rendre variable, par exemple, en l’incrémentant tous les 5 points.

3.7.2. Codage de la vitesse du serpent

  • Ecrire une fonction plus_vite(score) qui calcul la vitesse du serpent en fonction du score

def plus_vite(score):
	'''
	Calcul de la vitesse du serpent
	paramètre :
	  score : score en cours
	retourne :
	  vitesse du serpent entre 1 et 10
	'''

	# vitesse est le quotient de la division entière de score par 5 ( + 1)
	vitesse = ___________

	# Si vitesse est superieur à 10, alors vitesse = 10
	_____________________:
		vitesse = _______

	return vitesse
  • Modifier la fonction deplacement() pour qu’elle appelle la fonction plus_vite()

def deplacement(win, score, key, snake, food):

	...

	vitesse = plus_vite(score)

	win.timeout(150//vitesse)

	return snake, score
  • Vérifier le bon fonctionnement

4. On joue !

Amusez vous, quand vous en aurez assez, modifiez le scénario, les bords de la fenêtre deviennent mortel, on place plusieurs pommes, …​. Faites preuve d’imagination !


the_end.jpg