Étiquette : Jeux

NumApps

The Map, un jeu en python, NumWorks

Sur un plateau, vous devez vous déplacer pour trouver le trésor, mais sur votre chemin, vous pourriez bien rencontrer des bonus, des ennemis et des pièges. Arriverez-vous à traverser les différents niveaux et à trouver les trésors ?

Le principe du jeu

Dans ce jeu, vous contrôlez un personnage qui se déplace sur un plateau. Le but est d’atteindre la croix rouge, qui symbolise la fin du niveau. Le personnage que vous contrôlez à des statistiques de vie, d’attaque et de défense, et il doit parvenir vivant à la fin du niveau. Les points de vie du héros ne doivent pas tomber à 0, ce qui entraînerait sa mort. L’attaque du héros représente le nombre de dégâts infligés aux monstres par tour lors d’un combat, tandis que sa défense réduit le nombre de dégâts reçus par le héros.

Sur le plateau chaque case déclenche un évènement. Plusieurs événements sont possibles :

  • Case vide : Aucun événement.
  • Case marquée d’un cœur : Augmente les points de vie du héros.
  • Case marquée d’une épée : Augmente l’attaque du héros.
  • Case marquée d’un bouclier : Augmente la défense du héros.
  • Case marquée d’une épine : Réduit les points de vie du héros.
  • Case marquée d’une épée sur fond rouge : Déclenche un combat contre un monstre.

Sur le plateau, certaines cases sont visibles, tandis que d’autres restent cachées. Vous devez vous frayer un chemin tout en évitant les monstres et les pièges.

De nouvelles fonctionnalités seront ajoutées prochainement…

Commandes

◁ ▷
déplacer

Captures d’écran

Télécharger le jeu

Le premier lien vous permet d’accéder à la dernière version du jeu. En cas de problème, vous pouvez utiliser le deuxième lien :

NumApps

Memory – Countries, python, NumWorks

Le jeu de paires est un jeu de société qui est connu sous différents noms. Nous vous proposons ici une version revisitée pour réviser ses capitales européennes !

Gameplay

Ce jeu vous propose de tester vos connaissances géographiques.

  • Vous devrez associer 12 pays à leurs capitales respectives.
  • Les noms des pays et des capitales sont mélangés aléatoirement.
  • Votre mission est de reconstituer les bonnes paires.
  • Réfléchissez bien et tentez de faire un sans-faute !
  • Amusez vous tout en apprenant de nouvelles choses.
  • Prêt à relever le défi ?

Capture d’écran

Commandes

◁ △ ▷ et ▽OK
Déplacer le curseurSélection de la carte

Télécharger

Voici les liens pour télécharger le code python du jeu pour la NumWorks :

Tutoriels

Les bases pour débuter sur Unity

Envie de commencer à créer des jeux par vous-même? Unity est le meilleur moyen pour cela.

Unity, qu’est-ce que c’est ?

Unity est un moteur de jeu 2D et 3D gratuit faisant partie des plus utilisés de ces dernières années et est parmi les plus complets. De plus il possède une compatibilité avec tous les supports utilisés pou les jeux vidéos( PC, consoles, mobile) et a une prise en main assez simple donc parfait pour apprendre à faire des jeux

Commencer son projet sur Unity

Tous d’abord, il faut évidemment installer le Unity hub, que vous pouvez télécharger ici. Une fois l’installation terminée, nous pouvons enfin commencer notre premier projet. On commence donc par créer ce projet en appuyant sur « New project » et sélectionner si l’on veut faire à partir de zéro en 2D ou 3D ou si l’on part d’un template. Ici on va partir de zéro en 3D

Comprendre l’interface

Avant de travailler avec le moteur, il faut déjà comprendre l’interface dans laquelle on se retrouve afin de la maitriser. Cette interface se constitue de 6 parties

  • 1 > Scene View : La zone principale où on va placer et manipuler les objets .
  • 2 > Game View : Une vue d’aperçu pour tester à quoi ressemblera le jeu une fois exécuté.
  • 3 > Hierarchy : Liste des objets de la scène actuelle (caméra, lumières, personnages).
  • 4 > Inspector : Affiche les paramètres détaillés de l’objet sélectionné.
  • 5 > Project : Tous les fichiers (modèles, scripts…) sont stockés ici.
  • 6 > Console : Affiche les erreurs ou les messages liés à vos scripts.

À noter que la Scene view et la Game view s’affichent au même endroit suivant celui séléctionné, de même pour l’onglet projet et la console

Ajouter et manipuler différents objets

On va commencer par ajouter un cube à notre scène afin de pouvoir le manipuler par la suite. On va donc dans le panneau Hierarchy puis on fait un clic droit et on sélectionne « 3D object « puis « Cube ». Une fois le cube apparu à l’écran, nous pouvons commencer à le manipuler. Nous pouvons effectuer 2 actions basiques su ce cube: le déplacer sur 1 ou 2 axes à la fois (raccourci: W), le tourner sur un seul axe à la fois (raccourci: E) et enfin nous pouvons l’étirer sur 1 ou 3 axes à la fois(racourci: R)

On peut ensuite s’occuper des autres éléments de la scène, à commencer par la lumière. Il s’agit de l’objet « directional light » dans la Hierarchy. C’et un objet dont on ne peut changer la taille et dont la position ne change rien. Ainsi on utiliser uniquement la rotation pour orienter cette lumière comme celle du soleil en agissant sur sa hauteur dans le ciel et l’angle qu’elle possède par rapport au reste.

Enfin on va manipuler la caméra dont on ne peut également pas changer la taille. C’est cette caméra qui décide à partir de quelle position nous allons voir la scène créée , on peut observer par cette caméra dans la parti Game view mais également lorsqu’on la déplace, grâce à la petite fenêtre qui s’affiche en bas à droite et qui nous montre ce que l’on va voir à travers la caméra

Colorer un objet

On va pouvoir changer la couleur de notre objet et lui ajouter une texture plus ou moins réfléchissante. Pour cela, on va commencer par faire un clic droit dans le panneau Project et sélectionner « Create » puis « Material ». À partir de là on va modifier a texture en changeant plusieurs paramètres dans le panneau Inspector . Une fois que cela est fait, on va pouvoir appliquer cette texture aux objets que l’on veut en la glissant sur l’objet dans la Hierarchy ou directement dans la scène

Faire bouger l’objet grâce à un script C#

Pour pouvoir faire bouger nos objets il va bien évidemment nous falloir programmer (sinon ce serait trop facile). Unity utilise du code en C# pour ses programmes, il va donc falloir un éditeur compatible comme Visual Studio afin de pouvoir modifier ce script. On va donc commencer par créer le script en faisant clic droit>Create>C# script. Il est préférable de le nommer pour s’y retrouver plus facilement lorsqu’on en aura plusieurs. Une fois le fichier créé, on va l’ouvrir afin de le modifier. Pour l’exemple, j’ai décidé de faire simplement tourner l’objet sur lui-même grâce au code ci-dessous. Une fois le code écrit et sauvegardé, on va pouvoir l’intégrer à un objet en sélectionnant l’objet dans le panneau Hierarchy et en glissant le fichier vers l’Inspector . On peut ensuite lancer la scène avec le script en appuyant sur les boutons en haut au centre de l’écran

public class ObjectRotator : MonoBehaviour
{
    // Vitesse de rotation 
    public Vector3 rotationSpeed = new Vector3(0, 100, 0);

    void Update()
    {
        // Appliquer la rotation
        transform.Rotate(rotationSpeed * Time.deltaTime);
    }
}

À partir de là vous avez des bases sur lesquelles vous pouvez encore progresser sur tous les aspects de la conception de jeu , c’est une expérience très intéressant car cela nous pousse à en apprendre plus pour aller plus loin et pas seulement en code mais également en game design, éventuellement en modélisation et en encore une multitude d’autres aspects.

Merci d’avoir suivi ce tutoriel, j’espère que vous avez pu en apprendre plus sur l’incroyable moteur qu’est Unity

Tutoriels

Apprendre à développer un jeu simple sur Scratch 2.0

Ce tutoriel a pour objectif de vous permettre de découvrir l’univers de Scratch en expliquant de façon simple le fonctionnement de chaque bloc/instruction ainsi que de pouvoir créer pas à pas votre propre jeu pour donner un côté ludique à votre apprentissage.

Apprendre les éléments importants de Scratch

Dans Scratch, les blocs sont stockés dans des Sprites ou des arrière-plans. Les blocs de Scratch 2.0 sont répartis en 9 catégories, mais nous allons parler seulement les blocs des 8 premières catégories dans ce tutoriel.

Première catégorie : Mouvement

La catégorie Mouvement contient tout les blocs nécessaires au déplacement d’un Sprite.

Ce bloc permet de déplacer un Sprite de x pas sur un axe selon l’orientation du Sprite.

Ces blocs permettent d’orienter un Sprite de x° vers la droite ou vers la gauche.

Ce bloc permet de positionner le Sprite à des coordonnées aléatoires ou aux coordonnées du pointeur/curseur de la souris.

Ce bloc permet de positionner le Sprite à des coordonnées ( x ; y ).

Ce bloc permet de déplacer le Sprite à des coordonnées aléatoires ou aux coordonnées du pointeur/curseur de la souris pendant x seconde.

Ce bloc permet de déplacer le Sprite à des coordonnées ( x ; y ) pendant x seconde.

Ce bloc permet d’orienter le Sprite à x° dans une intervalle de [-180 ; 180].

Ce bloc permet d’orienter le Sprite vers le pointeur/curseur de souris.

Ces blocs permettent de modifier la position du Sprite respectivement en modifiant l’abscisse x ou en modifiant l’ordonnée y.

Ces blocs permettent de modifier la position du Sprite respectivement en ajoutant une valeur à l’abscisse x ou en ajoutant une valeur à l’ordonnée y.

Ce bloc permet d’empêcher le Sprite de sortir du cadre en ajoutant 180° à son orientation si un bord est atteint.

Ce bloc permet de fixer un sens de rotation pour le Sprite.

Ces blocs permettent d’afficher des variables qui expriment respectivement l’abscisse, l’ordonnée et l’orientation (en °) du Sprite.

Seconde catégorie : Apparence

La catégorie Apparence contient tout les blocs nécessaires à la modification de l’apparence d’un Sprite ou d’un arrière-plan.

Ces blocs permettent d’afficher une chaîne de caractères sous forme de bulle de texte (comme dans les bandes dessinées) proche du Sprite durant un temps donné ou pour toujours.

Ces blocs permettent de changer le costume du Sprite.

Ces blocs permettent de changer d’arrière-plan.

Ce bloc permet d’ajouter une valeur en pourcent à la taille du Sprite.

Ce bloc permet de définir une taille en pourcent d’un Sprite.

Ce bloc permet de modifier le costume d’un Sprite en ajoutant une valeur aux effets disponibles.

Ce bloc permet de modifier le costume d’un Sprite en définissant une valeur aux effets disponibles.

Ce bloc permet de supprimer les effets graphiques (c’est-à-dire, remettre à 0 les valeurs associées à chaque effet).

Ce bloc permet d’e montrer d’afficher ou de cacher un Sprite.

Ce bloc permet de positionner un Sprite devant ou derrière l’arrière-plan.

Ce bloc permet de changer d’arrière-plan de façon ordonnée.

Ces blocs permettent d’afficher les variables associées respectivement à l’indice du costume du Sprite, à l’indice de l’arrière-plan et à la taille en pourcent du Sprite.

Troisième catégorie : Son

La catégorie Son contient tout les blocs nécessaires à l’ajout et à la modification des sons de votre jeu.

Le premier bloc permet de jouer un son définit, empêchant toutes autres instructions de fonctionner à la suite de ce bloc jusqu’à ce que le son finissent. Le second bloc permet de jouer le son tout en permettant les autres instructions à la suite de fonctionner.

Ce bloc permet de stopper subitement tous les sons.

Ce bloc permet de modifier le son en ajoutant une valeur aux effets disponibles.

Ce bloc permet de modifier le son en définissant une valeur aux effets disponibles.

Ce bloc permet de modifier le volume (en pourcent) d’un son.

Ce bloc permet de supprimer les effets sonores (c’est-à-dire, remettre à 0 les valeurs associées à chaque effet).

Ce bloc permet d’afficher une variable associée à la valeur du volume en pourcent.

Quatrième catégorie : Evénements

La catégorie Evénements contient tout les blocs nécessaires au démarrage d’un script.

Ces blocs permettent de démarrer un script selon certaines conditions.

Ces blocs permettent de démarrer les scripts associés au bloc « quand je reçois message1 », bloquant (bloc de droite) ou non (bloc de gauche) les instructions à la suite de ces blocs.

Cinquième catégorie : Contrôle

La catégorie Contrôle contient tout les blocs nécessaires au contrôle des scripts du programme sous certaines conditions.

Ce bloc permet d’attendre 1 seconde avant de lancer les instructions à la suite.

Ces blocs permettent de répéter x fois ou indéfiniment un script.

Ces blocs permettent d’imposer une condition pour démarrer leur script. Pour le second bloc uniquement, si la condition n’est pas rempli, un autre script est démarré.

Ce bloc permet d’attendre avant de lancer les instructions à la suite jusqu’à ce qu’une condition soit rempli.

Ce bloc permet de répéter indéfiniment un script jusqu’à ce qu’une condition soit rempli.

Ce bloc permet d’arrêter certain script ou d’arrêter le programme entier.

Le premier bloc permet de créer un clone du Sprite associé à ce bloc. Le second bloc permet le clonage d’un Sprite définit.

Ce bloc permet de débuter un script si le Sprite est un clone.

Ce bloc permet de supprimer le clone associé à ce bloc.

Sixième catégorie : Capteurs

La catégorie Capteurs contient tout les blocs nécessaires pour élaborer des conditions et créer des interactions dans un programme.

Ce bloc permet de détecter si le Sprite associé touche le pointeur/curseur de la souris, le bord, etc…

Ces blocs permettent de détecter si le Sprite associé touche une couleur ou si une couleur en touche une autre.

Ce bloc est la variable associé à la valeur de la distance entre le Sprite associé et le pointeur/curseur de la souris.

Ce bloc permet au Sprite associé de poser une question définit et permet d’attendre avant de lancer les instructions à la suite jusqu’à ce que ce qu’une réponse soit donnée.

Ces blocs permettent de détecter si une touche définit est pressée ou si la souris est pressée.

Ces blocs sont les variables associées au coordonnées ( x ; y ) du pointeur/curseur de la souris.

Ce bloc permet au Sprite associé, en mode glissade, d’être déplacé en jeu par le joueur et, en mode non glissade, de ne pas être déplacé en jeu par le joueur.

Ce bloc permet de remettre à 0 la valeur associée au chronomètre.

Ce bloc est la variable associé à la valeur du nombre de jour depuis l’an 2000.

Ces blocs permettent d’afficher les variables associées respectivement au réponse donné, au volume sonore, au chronomètre, au nom d’utilisateur et à l’année, le mois, la date, le jour de la semaine, l’heure, les minutes ou les secondes.

Septième catégorie : Opérateurs

La catégorie Opérateurs contient tout les blocs nécessaires pour effectuer des opérations mathématiques, pour générer l’aléatoire et pour analyser des chaînes de caractères.

Ces blocs permettent respectivement de faire des additions, soustractions, multiplication et division de valeurs.

Ce bloc permet de choisir une valeur aléatoire dans un intervalle de valeur définit.

Ces blocs permettent respectivement de dire qu’une valeur définit est supérieure à une autre, qu’une valeur définit est inférieure à une autre et qu’une valeur définit est égal à une autre.

Le premier bloc permet de vérifier si les 2 équations qu’il contient sont vrais. Le second permet de vérifier si 1 des 2 équations qu’il contient est vrai.

Ce bloc permet de d’inverser la valeur de vérité de ce qu’il contient (par exemple: 5-5=0 est vrai donc non(5-5=0) est faux)

Ce bloc permet de lier 2 chaînes de caractères.

Ce bloc permet de citer la lettre associée à l’indice demandé.

Ce bloc permet de dire le nombre de caractères contenu dans une chaîne de caractères définit.

Ce bloc permet de vérifier si une chaîne de caractères définit contient un caractère définit.

Ce bloc renvoie le reste de la division entière (aussi appelée division euclidienne) de la première entrée (le numérateur) divisée par la seconde entrée (le dénominateur).

Ce bloc permet d’arrondir une valeur définit.

Huitième catégorie : Variables

La catégorie Variables contient tout les blocs nécessaires à la création et la modification des variables créées.

Ce bloc permet de définir la valeur associée à « ma variable ».

Ce bloc permet d’ajouter une valeur définit à la valeur associée à « ma variable ».

Ces blocs permettent d’afficher ou de cacher la valeur associée à « ma variable ».

Ce bloc représente « ma variable ».

Créer son jeu

Maintenant, nous allons vous montrer pas à pas comment créer un petit jeu en environ 30-60 minutes.

Tout d’abord, supprimez le Sprite de base qui est présent quand vous lancez Scratch.

Ensuite, allez chercher l’arrière-plan « blue sky ».

Puis, allez chercher le Sprite « monkey ».

Maintenant, ajouter ce script au singe. Il permet de définir un emplacement et une taille pour le singe à chaque lancement du jeu.

Ajouter ce script au singe. Il permet de déplacer le singe à gauche ou à droite avec les flèches de votre clavier.

Allez chercher le Sprite « Bananas » et le Sprite « Watermelon ».

Ajouter ce script à la banane. Il permet de définir un emplacement et une taille pour la banane à chaque lancement du jeu ainsi que de permettre la chute de la banane pour que le singe puisse la collecter.

Idem pour la pastèque.

Maintenant, vous allez dans la catégorie « Variables » et vous allez créer la variable « Banane » et la variable « Vie ».

Ajouter le script de droite à la banane. Il permet d’ajouter à la variable Bananes des points à chaque fois qu’une banane est collectée.

Ajouter ce script à la pastèque. Il permet de retirer à la variable Vie des points à chaque fois qu’une pastèque est touchée.

Ajouter ce script au singe. Il permet de changer l’apparence du singe à chaque fois qu’il prend des dégâts.

Ajouter ces scripts au cœur. Le script de gauche permet de définir un emplacement et une taille pour le cœur à chaque lancement du jeu ainsi que de permettre la chute du cœur pour que le singe puisse le collecter. Le script de droite permet d’ajouter à la variable Vie des points à chaque fois qu’un cœur est collectée.

Ajouter ces scripts au singe. Ils permettent d’arrêter le jeu si le nombre de vie atteint 0.

Ajouter ce script à la banane, la pastèque et au cœur. Ils permettent d’arrêter leur programme respectif.

Voici donc ce que vous devriez avoir au final.

Script final du singe:

Script final de la banane:

Script final de la pastèque:

Script final du cœur:

Félicitations, vous avez atteint la fin de ce tutoriel, j’espère qu’il vous aura était utile. Sinon, il existe un autre tutoriel sur Scratch 2.0 disponible sur le site nsi.xyz, dans l’onglet tutoriels où vous vous trouvez actuellement, que je vous conseille fortement de regarder pour pouvoir créer un jeu plus complexe sur Scratch 2.0. Merci d’avoir lu cette article.

Tutoriels

Comment créer un jeu sur scratch ?

Vous avez toujours voulu créer un jeu vidéo par vos propres moyens ? Si c’est le cas vous êtes au bon endroit. Créer un jeu vidéo est généralement très compliqué et impose de nombreuse propriétés inconnu du grand public, je vais ainsi vous montrer un exemple de jeu sur Scratch comme vous pouvez le voir ci dessous

Pour commencer il faut d’abord soit sélectionner un arrière plan ou en créer un comme dans mon cas ci dessus. Il faut ensuite choisir des personnages et les modifier pour rendre le jeu de plus en plus attractif, par exemple j’ai modifié le costume de mon personnage pour le mettre avec le maillot du Réal de Madrid

Maintenant que le personnage est customisé il faut créer son code qui va permettre de faire le jeu. Le code ci dessous permet d’animer le personnage pour le rendre mobile en le changeant de costume. L’objectif de ce code est de faire taper le joueur dans le ballon et de faire une célébration en cas de victoire atteinte pour un score de 10 buts marqués en 20 tirs

Le but de la création de ce jeu est de simuler des tirs au but il faut donc ajouter un autre personnage qui servira de gardien qui a pour but d’arrêter le tir de l’attaquant. Il faut ensuite créer un code comme ci dessous qui lui permet de se déplacer avec les touches X pour se déplacer vers la gauche, C pour se déplacer vers la droite, W pour plonger à gauche et V pour plonger à droite. La barre espace permet de faire sauter le gardien.

Pour que le jeu puisse réellement fonctionner il faut ajouter les commandes du tireur, le ballon, et le compteur de points. Tout ceci va être exécuté sur le script du ballon ci dessous :

Ce script permet d’annoncer les buts, les arrêts et les loupés car si le ballon touche le gardien c’est un arrêt, si le ballon est en dehors du cadre c’est loupé sinon c’est but. Ce code permet à l’attaquant de tirer avec le viseur de souris. Ce jeu est un jeu pour 2 un tireur et un gardien, il peut être ajouté des options facultatives comme une animation de victoire ou de défaite que je ne vous montre pas car il faut essayer d’améliorer son jeu avec son inspiration et car cela permet aussi d’apprendre à coder par soi même

Vous pouvez maintenant jouer au jeu à 2 et aussi le publier sur Scratch

Projets

Un Casse-briques sur ta NumWorks !

Le jeu-vidéo mythique des années 70 ! Casse-briques est un jeu de réflexion qui vous demandera beaucoup d’agilité. Le principe est simple, une balle est lancée, utilisez la plateforme mobile mis à votre disposition pour la faire rebondir et tentez de toucher les briques.

Genèse du projet

Le Casse-briques sur NumWorks est un jeu que j’ai voulu initialement développé il y a de cela un peu moins d’un an, juste après la publication de l’application Colors. J’avais commencé très brièvement la partie graphique du jeu, mais c’était assez rudimentaire et ça ne plaisait pas trop. Ensuite l’été a débuté et suite à des problèmes techniques lors du développement, le projet a peu à peu pris la poussière… Jusqu’à récemment.

Déterrage

Pour le dernier projet de l’année en NSI (le projet libre), j’ai décidé de sortir du placard le jeu, initialement abandonné, pour le redévelopper. Néanmoins, 2 semaines (pas à temps plein malheureusement, au contraire), ça reste peu pour développer un jeu de cette envergure, même pas l’IDE encore ouvert que j’avais énormément de questions qui me traverser l’esprit :

  • Comment faire bouger la balle et la faire rebondir correctement ? (rien que ça)
  • Comment gérer les collisions de la balle avec les briques ? (sans aucun doute la chose la plus complexe)
  • Comment générer des niveaux inédits et cohérents ?

C’est pour ça qu’en réalité, j’ai commencé doucement le développement du projet en avance (mi-mars), pour avoir pleinement le temps de proposer une réalisation finale convenable et fonctionnelle. J’avais énormément d’idées pour la conception du jeu, notamment avec un système de powers-up. Mais tout n’a pas pu voir le jour malheureusement.

La main à la pâte

Maintenant que les idées sont là, il faut retrousser ses manches et plonger dans la phase de développement.

L’ébauche d’un menu

Élaborons d’abord la page le menu du jeu, celui-ci devra afficher notamment, le score, le niveau, ainsi que le nombre de vies restantes. Et c’est le cas :

C’est une partie plutôt simple (on commencer doucement), rien de bien sorcier :

[...]
def menu():
    rec(0, 0, 320, 222, dark_c)
    rec(0, 200, 320, 22, game_c)
    for k in range(len(chars)):  # Display method inspired by @riko_schraf
        for j in range(19):
            if chars[k] >> j & 1 == 1:
                rec(110 + 12 * k + (j % 3) * 3, 40 + (j // 3) * 3, 3, 3, light_c)
    txt("Score: " + str(score), 160-5*(7+len(max(str(score), str(stage)))), 70, bright_c, dark_c)
    txt("Stage: " + str(stage), 160-5*(7+len(max(str(score), str(stage)))), 90, bright_c, dark_c)
    txt("Lives: " + str(pv), 160-5*(7+len(max(str(score), str(stage)))), 110, bright_c, dark_c)
    if pv > 0:
        txt("Press OK to play", 80, 150, bright_c, dark_c)
    else:
        txt("GAME OVER", 115, 150, bright_c, dark_c)
    txt("Gameplay by nsi.xyz/breakout", 20, 202, bright_c, game_c)

Une fonction menu(), assez simpliste dans le fond. L’affichage du nom du jeu « BREAKOUT » est géré entre les lignes 5 et 8. C’est un système d’affichage initié par Eric SCHRAFSTETTER (@riko_schraf), que j’ai modifié pour afficher le texte voulu, il utilise notamment la décomposition d’entiers en binaire pour savoir si une case est « allumée » ou non. Pour les plus déterminés et les plus curieux qui voudrons découvrir comment ça fonctionne, voici quelques ressources : Opération bit à bit — Wikipédia (wikipedia.org) [FR] • Python Bitwise Operators – GeeksforGeeks [EN]

Les fondations

Ensuite, voyons par quoi repose le jeu :

p_size = 40
p_speed = 3
p_x, p_y = 160, 210
p_color = bright_c

b_size = 7
b_dir = [2, -2]
b_pos = [160, 180]

br_width = 30
br_height = 15

game_speed = 0.01
game_state = "IN_MENU"

chars = (121579, 186351, 234191, 188271, 187117, 252783, 252781, 74903)

points = {"red": 100, "orange": 80, "yellow": 60, "green": 20}
stage, score, pv = 1, 0, 3
level = ""
cuboids = []

Ce sont (quasiment) toutes les variables globales utilisés dans le jeu, certaines sont plus intéressantes que d’autres, et nous nous attarderont ici uniquement sur celles que je juge donc intéressantes. Certaines variables respectent une certaine nomenclature (p_*: ce qui concerne la plateforme mobile, b_*: ce qui concerne la balle, br_*: ce qui concerne les briques, etc.).

  • p_size: C’est la taille de la plateforme mobile
  • p_speed: C’est sa vitesse de déplacement (si elle vaut 3, c’est qu’elle va se déplacer de 3 en 3 pixels (vers la gauche ou la droite))
  • p_x: C’est la position x, de la plateforme mobile, plus exactement, c’est la position du pixel au centre de la plateforme mobile. La position y est moins importante, étant donné que celle-ci est constante (c’est pourquoi une list n’a pas été utilisé pour stocker ces deux valeurs).

La position et la taille de la plateforme sont des éléments essentiels pour détecter une collision.
Ici, p_x est est représenté par le pixel rouge :

De la même manière.

  • b_size: C’est le diamètre (même si on parlera ici plutôt de côté) de la balle.
  • b_dir: C’est un vecteur, qui va donc déterminer où la balle va se diriger (si il vaut [1, 1], la balle ira en diagonale vers la droite et vers le bas (l’axe des ordonnées est inversé)). Plus la norme du vecteur est élevé, plus la balle sera rapide (si il vaut [2, 2], il ira aussi vers la droite et vers le bas, mais la balle sera plus rapide).
  • b_pos: C’est la position x, y de la balle.

Ces 3 variables (surtout la 1ère et la 3ème) sont essentiels pour détecter une collision.

Comme vous le voyez, le jeu a été conçu pour très facilement modulable. Pourquoi ? Car déjà c’est préférable lorsqu’on développe n’importe quelle application, et aussi pour intégrer des powers-up facilement. En effet, modifier la valeur de b_size aurait fait un power-up qui change la taille de la balle en plein jeu !

Des collisions

C’est sans aucun doute ce qui m’a mis le plus en difficulté, j’ai voulu faire un système non spécifique au jeu, qui serait donc facile d’intégrer dans un autre jeu, et c’est ce que j’ai fait.

Grosso modo, les collisions fixes (le système n’est pas optimisé pour gérer des entités mouvantes, celles de la plateforme mobile sont gérés autrement et sont spécifiques au jeu) sont stockées dans une list, enfin plutôt dans une list elle-même dans une list, enfin non, plutôt dans une list elle-même dans une list qui est elle-même dans une list. 🤯 😵‍💫
J’ai dit que les collisions étaient stockées, mais qu’est-ce que j’appelle une collision ? En réalité je stocke 2 valeurs entières qui vont former un cuboïde, ce dernier sera interprété comme un masque de collision (hitbox).

Ici le rectangle noir représenté un masque de collision, pour pouvoir le retrouver, nous avons besoin au minimum des coordonnées du point supérieur gauche et du point inférieur droit. Ainsi si la position de la balle se trouve dans la zone, elle doit rebondir. En réalité c’est un peu plus compliqué que ça, car selon le côté du cuboïde où la balle atterrit, la balle ne part pas au même endroit… Il faut donc aussi déterminer le côté du cuboïde où la balle est entré en collision. En bref, ce sont énormément de mathématiques.

[...]
def collision_manager(x, y):
    for i in range(len(cuboids)):
        if cuboids[i] and (i < cuboids[i][0][1]+10 or len(cuboids)-3 <= i <= len(cuboids)):
            h = b_size//2
            x_s, y_s, x_e, y_e = cuboids[i][0][0]-h, cuboids[i][0][1]-h, cuboids[i][1][0]+h, cuboids[i][1][1]+h
            if y_s-abs(b_dir[1]) <= y <= y_s+abs(b_dir[1]) and x_s <= x <= x_e:
                b_dir[1] = -b_dir[1]
                destroy_brick(i)
            elif x_s-abs(b_dir[0]) <= x <= x_s+abs(b_dir[0]) and y_s <= y <= y_e:
                b_dir[0] = -b_dir[0]
                destroy_brick(i)
            elif x_e-abs(b_dir[0]) <= x <= x_e+abs(b_dir[0]) and y_s <= y <= y_e:
                b_dir[0] = -b_dir[0]
                destroy_brick(i)
            elif y_e-abs(b_dir[1]) <= y <= y_e+abs(b_dir[1]) and x_s <= x <= x_e:
                b_dir[1] = -b_dir[1]
                destroy_brick(i)

Voici une démonstration du système (pendante la phase de développement) avec quelques informations de débogage affichés en haut à droite :

Les masques de collisions sont représentés par des rectangles rouges.

Génération de niveaux

Le système de générations de niveaux a été pensé par Vincent ROBERT (@nsi.xyz), ce système utilise également les mathématiques (comme tous les systèmes du jeu en fait).

[...]
def level_generator(stg):
    lvl = "".join([" " for _ in range(10)])+"*"*(stg//11+1)*10
    col = set()
    for i in (2, 3, 5, 7):
        if not stg % i:
            for j in range(i, 10, i):
                col.add(j)
    for i in range(stg // 11 + 2):
        for j in range(10):
            if j in col:
                lvl = lvl[:i * 10 + j] + " " + lvl[i * 10 + j + 1:]
    return lvl

Un niveau est stocké dans un str, à l’aide d’un « motif », le « motif » est composé de « * » et de  » « , lorsqu’il y a un « * », c’est que c’est l’emplacement d’une brique, sinon c’est qu’il y en a pas. Ainsi, générer un niveau consiste à générer une chaine de caractères avec des « * »,  » « . À noter, qu’ici, utiliser une chaine de caractère n’est pas très pertinent, il aurait été préférable d’utiliser un entier, et d’utiliser sa notation binaire (1: emplacement d’une brique, 0: emplacement vide). Peut-être que ce sera optimisé dans une version future du jeu (avec l’intégration des powers-up).

Ensuite, le niveau est construit :

[...]
def level_constructor():
    global level, cuboids
    rec(0, 0, 320, 222, dark_c)
    level = level_generator(stage)
    for i in range(len(level)):
        if level[i] == "*":
            rec(1 + (br_width + 2) * (i % 10), 1 + (br_height + 2) * (i // 10), br_width, br_height, set_color(i // 10))
    cuboids = [[[1+(br_width+2)*(i % 10), 1+(br_height+2)*(i//10)], [1 + (br_width + 2) * (i % 10) + br_width,
                1 + (br_height + 2) * (i // 10) + br_height]] if level[i] == "*" else [] for i in range(len(level))]
    cuboids += (((0, 0), (0, 222)), ((0, 0), (320, 0)), ((320, 0), (320, 222)))

Lorsqu’une brique est dessiné, ses collisions sont directement ajoutés dans la liste des collisions (cuboids).

Fonction mère

Enfin, les interactions du joueur sont directement interprétés dans la fonction engine(), Si on appuie sur la flèches gauche, on déplace la plateforme vers la gauche, et si c’est la flèche droite, on la déplace vers la droite. Lorsqu’il reste 3 collisions, c’est que toutes les briques sont cassés, et donc que le niveau est terminé ! Pourquoi 3 ? Car les collisions des bords de l’écran sont aussi stockés dans cuboids.

def engine():
    global p_x, pv, game_state, stage
    while game_state not in ("L_CLEAR", "G_OVER"):
        if keydown(0) and p_x > 10 + p_size // 2:
            p_x -= p_speed
            pad(p_size, 1)
        if keydown(3) and p_x < 310 - p_size // 2:
            p_x += p_speed
            pad(p_size, -1)
        ball_manager()
        if len(cuboids)-sum([1 if not i else 0 for i in cuboids]) == 3:
            game_state = "L_CLEAR"
            stage += 1
            pv += 1
        debug()
    pv -= 1 if game_state == "G_OVER" else 0
    menu()

Conclusion

Finalement, l’élaboration et le développement de ce projet ont été très enrichissantes. J’étais habitué jusqu’à ce projet à développer des applications « fixes » (avec un seul processus à gérer en même temps), développer des applications « dynamiques » (avec plusieurs processus simultanés (en l’occurrence, les mouvements de la balle, et celles de la plateforme)) est quelque chose d’un peu plus complexe, mais pas moins intéressant, au contraire ! Je suis assez satisfait du jeu final, même si je juge qu’il reste quelques défauts et qu’il manque des fonctionnalités que j’aurai voulu intégrer mais que je n’ai pas pu par manque de temps. Cependant, rien ne m’empêche de continuer le développement du jeu à l’avenir en y ajouter et corrigeant des choses !

À toi de jouer !

Ce jeu est disponible sur https://nsi.xyz/breakout !

NumApps

Breakout en python, NumWorks

Le jeu mythique des années 70 maintenant disponible sur NumWorks ! Sur Casse-briques, vous allez devoir casser toutes les briques de la partie grâce à la balle que vous renvoyez avec la plateforme sous votre contrôle !

Fonctionnalités

  • Plus de 50 niveaux uniques
  • Difficulté croissante au fil des niveaux
  • D’autres fonctionnalités sont prévus par la suite !

Captures d’écran

Commandes

◁ et ▷OK
DéplacerDémarrer

Pour aller plus loin

Si vous souhaitez en savoir davantage sur le fonctionnement et le développement de ce jeu, vous pouvez lire son compte rendu : Un Casse-briques sur ta NumWorks !

Télécharger

Nous vous proposons 2 liens distincts, le premier est le lien vers la source du créateur de l’application, le deuxième est un lien alternatif en cas de problème. Seul le premier lien garanti de disposer de la dernière version de l’application.

Projets

Un Sudoku sur ta NumWorks!

Le jeu de Sudoku est un jeu de logique joué sur une grille de 9 * 9 cases, divisée en 9 blocs de 3 * 3 cases. Le but est de remplir chaque case avec un chiffre de 1 à 9, en respectant certaines règles : chaque chiffre ne doit apparaître qu’une seule fois par ligne, colonne et bloc. Le jeu est terminé lorsque toutes les cases sont remplies ou lorsque toutes les vies ont été utilisées.

Logique du jeu

J’ai commencé par coder la couche logique du Sudoku. Pour stocker les informations, la grille est représentée par une matrice de 9 listes, contenant chacune 9 zéros.

board = reset()
def reset():
  return [[0 for i in range(9)] for j in range(9)]

Comme précédemment indiqué, le Sudoku doit respecter trois règles : chaque chiffre ne doit apparaître qu’une seule fois par ligne, colonne et bloc (un bloc étant un regroupement de 3 * 3 case). J’ai donc créé trois fonctions qui renvoient les éléments dans la ligne, la colonne et le bloc. Ensuite, j’ai créé une fonction qui appelle les trois précédentes et qui renvoie les nombres qui respectent les règles. Jusque-là, rien de bien compliqué.

def ligne(x):
  return [i for i in range(1,10) if i in board[x]]

def colonne(y):
  return [board[i][y] for i in range(9) if board[i][y] != 0]

def bloc(x, y):
  nums = []
  for i in range(x//3*3, x//3*3+3):
    for j in range(y//3*3, y//3*3+3):
      if board[i][j] != 0:
        nums.append(board[i][j])
  return nums

def possible(x, y):
  return [i for i in range(1,10) if i not in ligne(x)+colonne(y)+bloc(x,y)]

Il faut maintenant s’attaquer au plus complexe : créer la grille de Sudoku.

J’avais commencé par concevoir une grosse fonction qui créait bel et bien une grille valable de Sudoku, mais cette fonction était récursive (dans mon cas, elle s’appelait elle-même tant que la grille n’était pas valable). Et si sur ordinateur le jeu pouvait tourner (via un environnement de développement comme Thonny), ce n’était pas le cas sur Numworks où le nombre d’itération est très limitée.

Pour régler ce problème, j’avais modifié cette fonction qui faisait alors apparaître des nombres aléatoires présents dans la liste générée par possible(x, y). Par exemple, je générais un premier nombre, la grille étant vide tous les nombres étaient valides, puis un autre… jusqu’à une cinquantaine de nombres.

Mais je suis alors arrivé à un problème majeur : même si tous les nombres générés étaient valides, certains cases vides n’étaient pas solvables, c’est-à-dire que l’on ne pouvait placer aucun nombre car les seuls nombres pouvant être théoriquement placé étaient déjà présents dans la ligne/colonne/bloc par rapport à la case non solvable.

Je me suis retrouvé dans une impasse et c’est à ce moment-là que M. Robert m’a proposé un code qui parcourt la matrice à l’aide de deux boucles while. Pour chaque emplacement, le code vérifie si au moins un nombre peut être placé. Si c’est le cas, un nombre aléatoire est choisi dans le résultat de possible(x,y). Si aucun nombre n’est valide, la boucle recommence à la ligne précédente. Si le nombre d’essais dépasse 42, la fonction se bloque et recommence depuis le début.

def genere_board():
  global board
  board = reset()
  x = y = essai = 0
  while x!=9:
    essai += 1
    if essai ==42: 
      x = essai =0
      board = reset()
    y = 0
    while y!=9:
      if possible(x,y)==[]:
        board[x] = [0 for i in range(9)]
        y = 8
        x = x-1
      else:
        board[x][y] = choice(possible(x,y))
      y +=1
    x +=1
  return board

Maintenant que ma fonction principale est prête, je peux commencer à supprimer des nombres pour que la grille puisse être résolue. Cette fonction va donc prendre une case au hasard, et si elle est remplie, elle supprime le nombre.

def suppr():
  for i in range(40):
    x, y = randint(0,8), randint(0,8)
    while board[x][y] == 0:
      x, y = randint(0,8), randint(0,8)
    board[x][y] = 0

Gestion graphique (affichage)

Tout d’abord, j’ai généré une grille de 9 * 9, mais pour chaque bloc (groupe de 3 * 3 cases), la couleur change. Voici donc les deux fonctions qui s’en occupent :

def couleur(x,y,p=0):
  if (x//3 + y//3) %2 == 1:
    return (180-42*p,140-35*p,225)
  else:
    return (255,200-30*p,125-85*p)
    
def grille():
   for x in range(9):
    for y in range(9):
      rect(2+x*(22), 2+y*(22), 20, 20, couleur(x,y,0))

La fonction grille() ne dessine donc que la grille, mais elle utilise la fonction couleur(x, y, p) pour attribuer une couleur à chaque bloc. Grâce à l’opérateur modulo, on peut savoir dans bloc la case que l’on souhaite dessiner se trouve, et donc lui assigner la bonne couleur (en jaune ou en violet).

Résultat:

Ensuite, je me suis occupée du déplacement sur la grille. J’ai donc utilisé un exemple fourni par les professeurs que j’ai adapté :

def wait(buttons=(0,1,2,3,4,52)): 
  while True:
    for i in buttons:
      if keydown(i):
        while keydown(i): True 
        return i
 
while True:
  key_pressed = wait()
  pos_old = list(pos) 
  if key_pressed == 1:  # flèche haut
    pos[1] = (pos[1]-1) % pmax[1]
  elif key_pressed == 2: # flèche bas
    pos[1] = (pos[1]+1) % pmax[1]
  elif key_pressed == 3: # flèche droite
     pos[0] = (pos[0]+1) % pmax[0]
  elif key_pressed == 0: # flèche gauche
    pos[0] = (pos[0]-1) % pmax[0]
  if key_pressed not in (42, 43, 44, 36, 37, 38, 30, 31, 32):
    deplacement()

La fonction wait() attend qu’une touche soit pressée, et grâce au module ion, elle retourne la clé (valeur numérique identifiant la touche) de celle-ci qui est enregistrée dans key_pressed. Ensuite, en fonction de la touche pressée, la position via la variable pos change. L’opérateur modulo permet d’éviter de dépasser les limites de la grille. Il permet de retourner au début d’une ligne ou colonne lorsqu’on a atteint la fin de celle-ci.

Afin de repérer la case sur laquelle on se situe, j’ai ajouté la fonction surbrillance(), appelée par la fonction deplacement():

def surbrillance(x, y, mode):
  for i in range(9):
    rect(2+i*(22), 2+y*(22), 20, 20, couleur(i, y, mode))
    rect(2+x*(22), 2+i*(22), 20, 20, couleur(x, i, mode))
    if board[i][y] != 0:
      dessine_nb(i, y, board[i][y], mode)
    if board[x][i] != 0:
      dessine_nb(x, i, board[x][i], mode)
  for i in range(x//3*3, x//3*3+3):
    for j in range(y//3*3, y//3*3+3):
      rect(2+i*(22), 2+j*(22), 20, 20, couleur(i, j, mode))
      if board[i][j] != 0:
        dessine_nb(i, j, board[i][j], mode)
        
def deplacement():
  x, y = pos_old
  surbrillance(x,y,0)
  x, y = pos 
  surbrillance(x,y,1)
  rect(2+x*(20+2), 2+y*(20+2), 20, 20,couleur(x,y,2))
  if board[x][y]!=0:
    dessine_nb(x,y,board[x][y],2)

surbrillance() va redessiner les cases de la ligne, la colonne et le bloc associés à la case, en utilisant la couleur, fournit par couleur(x, y, mode).

La fonction deplacement() va donc à chaque fois qu’elle sera exécutée :

  • Appeler surbrillance() en utilisant en paramètre les coordonnées de l’ancienne position et mode=0 ce qui va donc effacer la surbrillance précédente,
  • Rappeler la fonction avec les nouvelles positions, et mode=1 ce qui va dessiner la nouvelle surbrillance.

Résulat:

J’ai ensuite rajouté la détections des touches du pavé numérique de la calculatrice. Pour faire cela, j’ai précisé dans la fonction wait() pour le paramètre buttons les clés 42, 43, 44, 36, 37, 38, 30, 31 et 32 qui correspondent respectivement aux touches 1, 2, 3, 4, 5, 6, 7, 8 et 9.

Une fois cela fait, j’ai indiqué dans le while vu précédemment – celui qui gère changements de la variables pos (… Mais si, je parle du while qui s’occupe des déplacements) – pour chaque nombre l’exécution de la fonction is_valid() en prenant comme en paramètre la position de la case (x et y) et le nombre qui veut être rajouté, dépendant donc de la touche que l’on presse.

elif  key_pressed == 42: #1
    is_valid(pos[0],pos[1],1)

Ici, si la touche 1 est pressée (qui correspond a la clé 42), la fonction is_valid() est appelée avec comme paramètre nb qui vaut 1.

def is_valid(x,y,nb):
  if board[x][y]==0:
    if nb == solution[x][y]:
      dessine_nb(x,y,nb,1)
      board[x][y]=nb

def dessine_nb(x,y,nb,mode):
  txt(str(nb),x*22+7,y*22+3,"black",couleur(x,y,mode))

La fonction is_valid() vérifie donc que le nombre indiqué en paramètre peut être placé, et si c’est le cas, il sera ajouté dans la matrice et dessiné dans la case sélectionnée (celle dont les positions sont indiqués en paramètre).

Autres fonctionnalités

Maintenant que la base du jeu est prête, on peut rajouter d’autres fonctionnalités. On va rajouter un système de difficulté et un système d’aide, que l’on pourra paramétrer via un menu. On va également rajouter un système de vie, pour que le jeu soit plus intéressant.

Je montrerai uniquement le principe pour la gestion des niveaux de difficulté, mais l’idée est la même pour le paramétrage de l’aide :

def draw_level(s=0):
  txt("Find", 237,35,(42+106*s,42+71*s,42+180*s))
  txt("< "*s+ "  "*(s==0) + str(lvl)+ " >"*s + "  "*(s==0), 225, 50,(42,)*3)
  
draw_level(1)
options = [32, 42, 52, 62]
diff = None
pos = 0

def select_diff():
  txt(str(options[pos]), 246, 50,(42,)*3)
  
while diff is None:
  key_pressed= wait()
  if key_pressed == 3: # fleche droite
    pos = (pos + 1) % len(options)
    select_diff()
  elif key_pressed == 0: # fleche gauche
    pos = (pos - 1) % len(options)
    select_diff()
  elif key_pressed in (4,52): # OK
    lvl= options[pos]
    break

Premièrement draw_level() permet de dessiner l’affichage du menu, si s=1 alors Find est en violet et les flèches apparaissent, sinon Find est en noir et les flèches ne s’affichent pas. Cette fonction va être appelée plusieurs fois afin de changer l’apparence de la sélection.

Dans la liste options est stockée les 4 difficultés. L’utilisateur peut alterner entre les 4 valeurs de la liste via les flèches gauche et droite grâce à la boucle et la condition while diff is None. Lorsque le joueur appuiera sur la touche OK, il confirmera sa sélection et quittera la boucle, afin de continuer la suite des instructions du programme. C’est en effet le même principe que la boucle pour gérer les déplacements.

Par la suite, la fonction suppr(lvl) est appelée (expliqué précedamment). Ainsi, le nombre de chiffre retiré de la matrice (la grille en somme) dépend du choix du joueur parmi les 4 proposés.

Comme je l’ai dit, l’idée est la même pour le choix de l’aide, sauf qu’au lieu d’avoir 4 choix, il n’y en a que deux : « On » ou « Off ».

Si « On » est sélectionnée, « Valid Number » apparait en bas à droite, et pour chaque déplacement dans la grille, aide() est appelée :

def aide(x,y):
  rect(200,155 ,200,20,(255,)*3)
  nbs = possible(x, y)  
  if board[x][y]==0:
    for i in range(len(nbs)):
      txt(str(nbs[i]), 265 + i * 20- len(nbs) *10, 155,(42,)*3)
      if nbs[i]!=nbs[-1]:
        txt(",", 275+ i * 20 - len(nbs) *10, 155,(42,)*3)
  else:
    txt("Full", 240,155)

Si la case sélectionnée est pleine, elle affiche « Full », sinon elle affiche tous les nombres qui pourrait être valables dans une case, générés par possible(x, y) vu précédemment.

On a presque mis en place toutes les fonctionnalités supplémentaires mais il manque encore une gameover quand le jeu se termine et le système de vie :

def check():
  c=0
  for i in range (9):
    for j in range (9):
      if board[i][j]!=0:
        c+=1
  return c==81

def coeur():
  rect(x+3,y+0,3,3, col)
  rect(x+15,y+0,3,3,col)
  rect(x+0,y+3,9,3,col)
  rect(x+12,y+3,9,3,col)
  rect(x+0,y+6,21,3,col)
  rect(x+3,y+9,15,3,col)
  rect(x+6,y+12,9,3,col)
  rect(x+9,y+15,3,3,col)
  
def perdre_vie():
    rect(272 - vie *25, 5, 21, 21, (255,) * 3)

La fonction perdre_vie() est appelée dans is_valid(). Si le nombre n’est pas juste, on retire une vie en décrémentant la variable vie de 1 et la fonction perdre_vie() est appelée. Elle dessine un carré blanc, afin d’effacer un des trois cœurs, dessiné au lancement du jeu avec coeur(222,5), coeur(247,5) et coeur(272,5).

La fonction check() regarde si la grille est remplie entièrement. Cette fonction est appelée à chaque déplacement dans la boucle principale while True :

while True:
  if vie==0:
    txt("Perdu!",230,5,(42,)*3,(255,)*3)
    break
  if check()==True :
    txt("  Gagné!  ",210,5,(42,)*3,(255,)*3)
    break

Ainsi si check() == True, le texte  » Gagné!  » apparait et la partie se termine grâce au break. Si le jeu se termine parce que l’utilisateur n’a plus de vie, c’est plutôt le texte « Perdu! » qui apparait.

Captures d’écran finale

Conclusion

Le jeu est donc fini ! La conception m’a pris relativement beaucoup de temps, mais ca m’a permit d’apprendre plein de chose ainsi que de m’améliorer en Python !

Si vous souhaitez le télécharger, voici le lien vers le script avec en supplément un lien vers la page de présentation du jeu :

Projets

Un reversi sur ta NumWorks !

Un jeu de société existant sous l’ombre des dames et des échecs, il est temps de rendre justice ! Le Reversi est un jeu de société pour deux joueurs qui se joue sur un plateau de 64 cases. Les joueurs placent tour à tour des pions noirs ou blancs sur le plateau, en capturant les pions adverses et en retournant les pions de l’adversaire pour gagner le contrôle du plateau.

À la découverte d’un nouveau jeu !

« – Alors qu’avons-nous là… Un Tetris ? Déjà fait et étonnement, pas le niveau. Un 2048 ? 😤 Une bataille navale ? Oula, pas certain de ce coup. Bon alors, quel jeu faire sur la NumWorks qui n’est pas trop dur à réaliser, mais reste néanmoins intéressant ?
– « Le Reversi » ?
– À vos souhaits.
– Tu n’as pas la lumière à tous les étages toi… C’est un jeu de société également appelé Othello. Voici plus de détails concernant les règles.
– D’accord, je vois le style, et bien allez, c’est parti ! »

Une base graphique

Il faut commencer quelque part ! Tout d’abord, on récupère un exemple proposé par notre professeur. Moins on en fait, mieux on se porte. 😎

Maintenant, il faut l’arranger pour notre jeu. Pour commencer, on s’occupe de placer une grille au centre de l’écran, ce sera notre plateau de jeu.

[...]
def grille(x, y, t_x, t_y, c, col):
  for w in range(t_x):
    for h in range(t_y):
      rec(1+x+w*(c+2), 1+y+h*(c+2), c, c, col)

La fonction est assez simple, elle va simplement faire un nombre de petits carrés indiqués en paramètres. t_x pour la taille en horizontale et t_y pour la taille en vertical (il faut indiquer un nombre de carrés pour les 2 variables).

Ce n’est pas fini ! Même pas du tout 😥

On va s’occuper du système pour gérer les déplacements sur la grille en se basant sur ce qu’a proposé le prof dans son exemple. Avant ça, nous allons définir un paquet de variables qui nous serons utiles tout le long du code (et encore, cette liste n’est pas exhaustive) :

# Beaucoup... BEAUCOUP de couleurs...
col = ((100,203,111),(220,)*3,(60,)*3,(148,113,222),
       (242,0,0),(255,183,52),(255,)*3,(60,139,72),
       (90,)*3,(180,)*3,(160,)*3,(242,)*3)
# Différents paramètres, à commencer par la taille horizontale et verticale
# de notre grille (ce sont les var pour t_x et t_y)
len_x,len_y = 8,8
# On l'utilisera plus tard, ce sera la position par défaut du sélectionneur de case
pos = [2,3]
# la taille des carrés dans la grille, en pixel
c = 20
# De sombres calcul... Pour centrer la grille !
x_g = (320 - (1+len_x*(c+2))) // 2
y_g = (200 - (1+len_y*(c+2))) // 2
# Très important ! C'est notre grille, chaque valeur d'une case est renseignée dans cette matrice
g = [[0 for i in range(len_x)] for i in range(len_y)]
# Gestion des tours, des scores,
# et des tours où un joueur ne peut pas jouer (allez check les règles pour comprendre ça) 
player_turn,sc_noir,sc_blanc,no_round = 2,0,0,0

Une partie de ces variables se situent dans une fonction init() qui permet de relancer une nouvelle partie lorsque la précédente est terminée et de gérer le système de paramètres (nous le verrons plus tard). Une grosse fonction play() s’occupe de vérifier les actions du joueur, c’est-à-dire les déplacements, s’il place un pion, etc.

def play():
  [...]
  while 1:
    key_pressed = None
    pos_old = list(pos)
    [...]
    key_pressed = wait()
    if key_pressed == 1:
      pos[1] = (pos[1]-1) % 8
    elif key_pressed == 2:
      pos[1] = (pos[1]+1) % 8
    elif key_pressed == 3:
      pos[0] = (pos[0]+1) % 8
    elif key_pressed == 0:
      pos[0] = (pos[0]-1) % 8
    elif key_pressed in (4,52):
      action(pos) # Simplifié
    if key_pressed != None:
      deplacement(pos_old)
  [...]

Ce code permet de gérer les déplacements sur toutes la grille et permet lorsque l’on appuie sur la touche Ok ou EXE, de valider notre sélection et d’exécuter une fonction action() qui s’occupe de lancer différentes fonctions selon certaines conditions pour vérifier le placement du pion puis le placer ou non.

Revenons sur nos déplacements. Avant toute chose, nous allons créer deux fonctions bien pratiques : getCase() et setCase(). Comme leur nom l’indique, l’une nous renvoie des informations concernant une case, l’autre change la valeur d’une case. « Récupérer » des informations ou « changer » la valeur d’une case, c’est modifier/récupérer une valeur dans notre matrice.

def getCase(g_in, x, y, col_mode=False):
  if col_mode:
    if 0 <= g_in[y][x] <= 2:
      return col[g_in[y][x]]
    return None
  return -1 if x < 0 or x >= len_x or y < 0 or y >= len_y else g_in[y][x]

def setCase(g_in_in, x, y, value):
  g_in_in[y][x] = value

On peut voir que getCase(), en plus de pouvoir nous renvoyer un statut pour la case sélectionnée (-1 si non valide, 0 si vide, 1 si blanc et 2 si noir), peut également nous renvoyer la couleur de la case. setCase() est plus simpliste mais tout aussi importante.
Voici notre fonction deplacement() :

def deplacement(pos_old=[],for_error=False):
  global c
  if not(for_error):
    if pos_old != []:
      x, y = pos_old
      if getCase(g,x,y) in (1,2):
        rec(x_g+1+x*(c+2),y_g+1+y*(c+2),c,c,col[0])
        cercle(x_g+8+(c+2)*x,y_g+3+(c+2)*y,getCase(g,x,y,True))
      else:
        rec(x_g+1+x*(c+2),y_g+1+y*(c+2),c,c,getCase(g,x,y,True))
    x, y = pos
    rec(x_g+1+x*(c+2),y_g+1+y*(c+2),c,c,col[7])
    cercle(x_g+8+(c+2)*x,y_g+3+(c+2)*y,col[1] if player_turn == 1 else col[2],1)
    case = getCase(g,x,y)
    if case != 0:
      cercle(x_g+8+(c+2)*x,y_g+3+(c+2)*y,getCase(g,x,y,True))
  else:
    rec(x_g + 1 + pos[0]*(c+2),y_g + 1 + pos[1]*(c+2), c, c, col[4])
    sleep(0.1542)

Cette fonction s’occupe de rendre le déplacement naturel et fluide. En effet, si l’on est à une case de coordonnée (1;1) et que l’on décide d’aller en (2;1), il faut que l’on affiche une surbrillance de sélection sur la nouvelle case, et que l’on rende à l’ancienne case son apparence d’origine. C’est très vulgarisé, car en plus de ça, ce code gère un affichage différents si le placement est incorrect, ainsi qu’une surbrillance différentes si on le joueur décide de se déplacer sur une case avec déjà un pion de placé.

On remarque une fonction cercle(). Elle a été proposé par le professeur, qui est chaleureusement remercié, car les pions, au lieu de ressembler à ça…

Petit faux raccord sur la couleur de la grille. Vous voyez également une des premières versions du sélectionneur, qui depuis a été bien amélioré.

…ressemblent plutôt à ça :

def cercle(x,y,col,s=0):
  for d in range(6):
    rec(x-d+(d==0)+2*s,y+d+(d==5)+2*s,6+2*d-2*(d==0)-4*s,16-2*d-2*(d==5)-4*s, col)

Que d’aventure… Nous avons maintenant un système de déplacement performant. Il faut maintenant s’attaquer à la logique du programme. On est à 21,73 % d’avoir fini le jeu !

Logique du jeux & IA

La partie la plus complexe. Je remercie mon père qui m’a beaucoup aidé pour cette fonction cœur de mon jeu. Cette fonction a deux modes de fonctionnement, elle peut vérifier si un pion peut être placé ; et placer un pion.

def do_pion(mode,pion_x,pion_y,directions_in,ai_ask=False):
  if getCase(g,pion_x,pion_y) != 0:
    return False
  result = []
  if ai_ask:
    dist_totale = []
  global sc_noir,sc_blanc
  pion_adverse = 1 if player_turn == 2 else 2
  for d in directions_in:
    x,y,dir_x,dir_y = pion_x,pion_y,d[0],d[1]
    current = getCase(g, x + dir_x, y + dir_y)
    dist_parcourue = 1
    while pion_adverse == current:
      x,y = x+dir_x,y+dir_y
      if mode == 1:
        sc_blanc += 1-2*(player_turn!=1)
        sc_noir += 1-2*(player_turn!=2)
        setCase(g, x, y, player_turn)
      current = getCase(g, x + dir_x, y + dir_y)
      dist_parcourue += 1
    if mode == 0 and dist_parcourue > 1 and current == player_turn:
      if not(ai_ask):
        result.append(d)
      else:
        dist_totale.append(dist_parcourue)
        result.append(d)
    else:
      continue
  if mode == 1:
    if player_turn == 1:
      sc_blanc += 1
    else:
      sc_noir += 1
    setCase(g,pion_x,pion_y,player_turn)
  else:
    if ai_ask:
      result.append(sum(dist_totale))
    return result

Lorsqu’elle vérifie si un pion peut être placé, elle va vérifier dans les 8 directions si la situation permet un retournement de pions adverses. Si c’est le cas, elle stocke la direction, et continue. À la fin, elle renvoie une liste contenant toutes les directions valides. S’il y a au moins une direction valide, la fonction est réexécutée, mais cette fois-ci, elle changera les valeurs dans la matrice en appliquant les règles de retournement. La fonction qui appelle do_pion() et qui analyse le résultat de la première exécution de celle-ci, c’est la fonction action() :

def action(position):
  global player_turn
  result = do_pion(0,position[0],position[1],directions)
  if result != [] and result != False:
    do_pion(1,position[0],position[1],result)
    player_turn = 1 if player_turn == 2 else 2
    update() # Nous verrons cette fonction un peu plus tard.
  else:
    deplacement(for_error=True)

La fonction do_pion() s’occupe toute seule d’appliquer quasiment toutes les règles du Reversi, elle s’occupe même de mettre à jour le score. Cependant, il y a une règle dans le Reversi qui stipule qu’un joueur ne peut pas jouer si il n’a aucun « mouvement légal », c’est-à-dire qu’il n’a aucun placement possible sur le plateau (fun fact : ce plateau est appelé othellier) qui respecte les règles du jeu. Si c’est le cas, son tour est passé. C’est le job de la fonction has_legit_hit() :

def has_legal_hit():
  legal_hits = []
  for y in range(len_y):
    for x in range(len_x):
      if do_pion(0,x,y,directions):
        legal_hits.append([x,y])
  return legal_hits

Plus précisément, elle vérifie seulement. C’est la fonction play() qui s’occupe de passer le tour du joueur :

def play():
  [...]
  if has_legal_hit() == []:
      player_turn = 1 if player_turn == 2 else 2
      no_round += 1
      if no_round == 2:
        affichage() # Nous verrons cette fonction un peu plus tard.
        break
      affichage() # bis
      continue
    else:
      no_round = 0
  [...]

Ce petit bout de code s’occupe également d’arrêter la partie, car si les deux joueurs ont dû passer leur tour l’un après l’autre, c’est qu’aucune des deux ne pourra rejouer.

Alors, en effet, si vous êtes au fond de la classe – je ne suis responsable de rien – vous pouvez jouer avec votre voisin, mais si vous êtes devant ? Et bien oui, il faut penser à tout le monde.

Notre IA va utiliser tous les composants décris précédemment. La seule action qu’elle va faire d’elle-même est de choisir une case valide. Dans la version actuelle (1.2.1), l’IA a deux niveaux : un où elle choisit parmi toutes les cases valides une case aléatoirement, et un autre où elle choisit la case qui lui rapporte le plus de point. Si vous avez bien été observateur, certaines fonctions telles que do_pion() avaient une partie qui était réservée pour une utilisation par l’IA. C’est dans do_pion() qu’est calculé quel est la case qui rapportera le plus de point.
Voici le code de la fonction ai() :

def ai(lvl):
  if lvl == 0:
    return choice(has_legal_hit())
  elif lvl == 1:
    legal_hits = has_legal_hit()
    temp = []
    plus_grand = [[],0]
    for i in range(len(legal_hits)):
      legal_hits[i] = [legal_hits[i],do_pion(0,legal_hits[i][0],legal_hits[i][1],directions,True)]
    for i in legal_hits:
      if i[1][-1] > plus_grand[1]:
        plus_grand = [[i[0]],i[1][-1]]
      elif i[1][-1] == plus_grand[1]:
        plus_grand[0].append(i[0])
    return choice(plus_grand[0])

Je suis sûr que vous ne savez pas quelque chose… Mais on est à 73,21 % du jeu là ! On y est presque !

Interface graphique, mise à jour de la grille, Game Over & système de paramètres

On y est presque et pourtant… Ce n’est pas encore fini 😵

C’est bien beau de faire des retournements et tout, mais moi ma grille est toujours vide sur mon écran !

Mettons en place une fonction update() qui va s’occuper de mettre à jour graphiquement la grille.

def update():
  global c
  for y in range(len_y):
    for x in range(len_x):
      if getCase(g,x,y) in (1,2):
        rec(x_g+1+x*(c+2), y_g+1+y*(c+2), c, c, col[0])
        cercle(x_g+8+(c+2)*x, y_g+3+(c+2)*y, getCase(g, x, y, True))
      # Ce else pourrait être facultatif et ferait une belle optimisation
      # si le code d'autres fonctions était adapté.
      # Il est ici depuis très longtemps et s'est fait oublié xD
      else:
        rec(x_g+1+x*(c+2), y_g+1+y*(c+2), c, c, getCase(g, x, y, True))
  deplacement()

Elle n’est pas très compliquée à comprendre. Je parcours toute la grille, je récupère la valeur de chaque case et je mets à jour ce qui doit apparaître (s’il doit y avoir un pion ou pas, et la couleur du pion).

Ensuite, le visuel est un peu pauvre, comme ceci sera bien mieux :

Il y a vraiment des délires sombres avec les couleurs…

Je remercie le professeur qui m’a aiguillé pour la conception de l’interface (après attention, il m’a juste suggéré d’aligner les informations avec des cases de la grille, ça va il a pas tout fait non plus 😂).

On ajoute un système permettant d’utiliser l’interface en début de partie pour paramétrer si on veut jouer en mode multijoueur ou contre IA, et le niveau de difficulté de l’IA :

Et un Game Over :

C’est M. ROBERT qui a joué cette partie, il est trop mauvais (🤪).

EDIT : Normalement je ne modifie pas les articles des élèves, mais je me dois de nier l’affirmation précédente qui porte atteinte à mon honneur. 😉

On assemble les pièces de puzzles correctement, et… Attendez, je crois que ça y est ! On a terminé ! Le jeu est fini à 100,42 % 😁

La page de présentation du jeu + Téléchargement

Remerciements

  • Mon père pour ses conseils et aide pour le développement du jeu,
  • M. ROBERT pour ses ressources,
  • ChatGPT pour certaines parties de l’article (en fait, juste le paragraphe de présentation de l’article).
Projets

Faites une partie de chasse au trésor sur la…

Partez à l’aventure et déterrez le trésor caché tout en prenant garde aux pièges dissimulés aux alentours ! Personnalisez également votre expérience grâce à trois seuils de difficulté et des cartes générées aléatoirement. Aurez-vous l’audace de vous mesurer à ce jeu de logique inspiré du démineur ?

Cliquez sur ce lien afin d’accéder aux règles du jeu.

Un projet sous couche graphique

Bien avant que le projet libre soit annoncé, j’avais créé un jeu de chasse au trésor sur Python, sous couche textuelle. La console de mon IDE affichait les différentes cases grâce à un print() et l’utilisateur devait entrer les coordonnées de la case à retourner.

Cette façon de procéder, bien qu’utile afin de tester un concept de jeu, demeure tout de même lente et peu accessible. Un jeu comme celui du démineur doit fournir un moyen rapide d’interagir avec lui. C’est la raison pour laquelle j’ai décidé de rénover ce jeu sous couche graphique, c’est-à-dire avec des sprites au lieu de chaînes de caractères.

Les bibliothèques Python

Tout au long du code sont exploitées deux bibliothèques Python : kandinsky et ion.

La bibliothèque kandisky propose deux fonctions utiles afin d’afficher des sprites et du texte : fill_rect(), qui trace un rectangle, et draw_string(), qui affiche du texte.

La bibliothèque ion, quant à elle, permet de détecter les touches poussées sur la calculatrice.

Le bout de code suivant importe les fonctions utiles de ces bibliothèques et crée des diminutifs afin de les appeler, en l’occurrence « fr » pour « fill_rect » et « ds » pour « draw_string » :

from kandinsky import fill_rect as fr
from kandinsky import draw_string as ds
from ion import keydown

Afin d’avoir un aperçu de mon code sur la calculatrice, j’ai installé sur mon IDE les plugins ion-numworks et kandinsky grâce à ce tutoriel.

La logique du jeu

Afin de bien coder ce jeu, il fallait que je comprenne la logique derrière celui-ci.

En effet, le jeu est divisé en deux plateaux : le plateau visible et le plateau caché.

En effet, le plateau visible est une grille de dimensions 5 x 5 remplie de chiffres. Le plateau caché, grille de mêmes dimensions, contient quant à lui le trésor, les piques et les couleurs. Les deux plateaux interagissent entre eux.

Le plateau caché serait initialisé en premier. Il recevrait le trésor et on placerait les couleurs en fonction de l’emplacement du trésor. Enfin, on positionnerait les piques et on changerait trois couleurs afin de brouiller les pistes.

Ensuite, on initialiserait le plateau visible et placerait les nombres en fonction de l’emplacement des piques. De ce fait, à chaque fois que le joueur retournera une case, une couleur, un pique ou le trésor sera affiché.

L’initialisation des plateaux

La réalisation du programme a commencé par la fonction init_plateau() qui, comme son nom l’indique, initialise les plateaux. Le plateau caché, dans le script, est représenté par la variable plateau_inf qui est une liste composée de cinq listes elles-mêmes composées de cinq éléments :

plateau_inf = [["V", "V", "V", "V", "V"],
               ["V", "V", "V", "V", "V"],
               ["V", "V", "V", "V", "V"],
               ["V", "V", "V", "V", "V"],
               ["V", "V", "V", "V", "V"]]

Ici, les listes sont remplies par la lettre « V » afin de ne pas avoir à placer la couleur verte. En effet, après avoir placé le trésor, les piques, les cases rouges et les cases orange, il ne reste plus que les cases vertes qui n’ont pas été touchées.

Ensuite, on place le trésor. Pour cela, j’utilise le module random et en particulier sa fonction randint() afin de choisir aléatoirement les coordonnées de la case :

def init_plateau():
    # -------------------- plateau inférieur
    # placement du trésor
    tresor_x, tresor_y = randint(0, 4), randint(0, 4)    
    plateau_inf[tresor_x][tresor_y] = "X"

Puis, on positionne les couleurs. Puisque la variable plateau_inf est remplie de « V », il nous suffit de placer des « R » et des « O » (pour rouge et orange).

Le bout de code suivant peut être plutôt hermétique. Ce dernier vérifie si les huit cases autour du trésor sont bien à l’intérieur de la grille, et si c’est le cas, on y place un « R ». Le même processus est adopté pour les seize cases autour des cases rouges afin d’y ajouter un « O » :

def init_plateau():
    # -------------------- plateau inférieur
    # placement du trésor
    # [...]
    # placement des cases rouges et orange
    for i in range(tresor_x - 2, tresor_x + 3):
        for j in range(tresor_y - 2, tresor_y + 3):
            if 0 <= i <= 4 and 0 <= j <= 4 and (i, j) != (tresor_x, tresor_y):
                if abs(tresor_x - i) <= 1 and abs(tresor_y - j) <= 1:
                    plateau_inf[i][j] = "R"
                elif abs(tresor_x - i) <= 2 and abs(tresor_y - j) <= 2:
                    plateau_inf[i][j] = "O"

Ensuite, on place les piques en prêtant bien attention à ce qu’un pique ne tombe pas sur le trésor et que deux piques ne soient pas sur la même case. On utilise la variable global nb_piques qui pourra être modifiée en fonction de la difficulté choisie :

nb_piques = [11]

def init_plateau():
    # -------------------- plateau inférieur
    # placement du trésor
    # [...]
    # placement des cases rouges et orange
    # [...]
    # placement des piques
    for _ in range(nb_piques[0]):
        while True:
            pique_x = randint(0, 4)
            pique_y = randint(0, 4)
            if plateau_inf[pique_x][pique_y] != "S" and plateau_inf[pique_x][pique_y] != "X":
                plateau_inf[pique_x][pique_y] = "S"
                break

Enfin, on met en place les fausses couleurs. Ce bout de code s’appuie sur beaucoup de conditions : il faut que la case choisie au hasard soit une case de couleur, et non un pique ou le trésor, et que cette case ne soit pas déjà une case colorée par une fausse couleur. Si ces conditions sont remplies, on peut changer la couleur de la case choisie :

def init_plateau():
    # -------------------- plateau inférieur
    # placement du trésor
    # [...]
    # placement des cases rouges et orange
    # [...]
    # placement des piques
    # [...]
    # placement des fausses couleurs
    for _ in range(nb_fausses[0]):
        fausses_originales = []
        while True:
            fausse_x, fausse_y = randint(0, 4), randint(0, 4)
            if plateau_inf[fausse_x][fausse_y] != "S" and plateau_inf[fausse_x][fausse_y] != "X":
                if (fausse_x, fausse_y) not in fausses_originales:
                    fausses_originales.append((fausse_x, fausse_y))
                    while True:
                        espace_nouv = randint(0, 2)
                        if espace_nouv == 0 and plateau_inf[fausse_x][fausse_y] != "V":
                            plateau_inf[fausse_x][fausse_y] = "V"
                            break
                        elif espace_nouv == 1 and plateau_inf[fausse_x][fausse_y] != "O":
                            plateau_inf[fausse_x][fausse_y] = "O"
                            break
                        elif espace_nouv == 2 and plateau_inf[fausse_x][fausse_y] != "R":
                            plateau_inf[fausse_x][fausse_y] = "R"
                            break
                    break

Et voilà ! Nous avons fini d’initialiser le plateau caché. Il ne reste plus que le plateau visible. Pas d’inquiétudes : celui-ci prend moins de temps à programmer. Il suffit de parcourir le plateau caché, et, lorsque l’on rencontre un pique, on ajoute 1 aux cases adjacentes du plateau visible (si elles se trouvent dans la grille) :

plateau_sup = [[0, 0, 0, 0, 0],
               [0, 0, 0, 0, 0],
               [0, 0, 0, 0, 0],
               [0, 0, 0, 0, 0],
               [0, 0, 0, 0, 0]]

def init_plateau():
    # -------------------- plateau inférieur
    # placement du trésor
    # [...]
    # placement des cases rouges et orange
    # [...]
    # placement des piques
    # [...]
    # placement des fausses couleurs
    # [...]
	# -------------------- plateau supérieur
    for ligne in range(0, 5):
        for colonne in range(0, 5):
            if plateau_inf[ligne][colonne] == "S":
                if ligne < 4: plateau_sup[ligne + 1][colonne] += 1
                if colonne < 4: plateau_sup[ligne][colonne + 1] += 1
                if ligne > 0: plateau_sup[ligne - 1][colonne] += 1
                if colonne > 0: plateau_sup[ligne][colonne - 1] += 1

La programmation du gameplay

À présent que les plateaux sont programmés, il faut permettre au jouer d’interagir avec eux. Cela se fait grâce à la fonction jeu().

Cette fonction peut être décomposée en deux parties : l’initialisation et la boucle.

Commençons par l’initialisation. Celle-ci permet de tracer la grille et afficher les chiffres et couleurs à l’écran :

def jeu():
    x, y = 0, 0
    reset()
    init_plateau()
    fr(0, 0, 320, 240, 'white')
    grille()

La fonction jeu() commence par instancier les coordonnées du curseur déplaçable. Ensuite, elle réinitialise les plateaux et autres variables grâce à la fonction reset(), appelle la fonction init_plateau() vue précédemment et trace une grille grâce à la fonction grille() (tirée de ce script).

def reset():
    for x in range(5):
        for y in range(5):
            plateau_sup[x][y] = 0
            plateau_inf[x][y] = 'G'
    cases_retournees.clear()
    cases_marquees.clear()
    vies[0] = 3

def grille():
    for i in range(6):
        if i < 6:
            fr(70, 31 + 35 * i, 176, 1, 'black')
        fr(70 + 35 * i, 31, 1, 176, 'black')

Souvenez-vous : « fr » est le diminutif de la fonction fill_rect() du module kandinsky. En vérité, on trace une multitude de rectangles très fins qui se croisent afin de créer la grille.

Les variables cases_retournees, cases_marquees et vies présentes dans la fonction reset() seront expliquées juste après.

Nous voilà à présent dans la boucle de la fonction. Cette boucle non-bornée se répète continuellement grâce aux mots-clefs « while True » :

vies = [3]

def jeu():
    # [...]
    while True:
        fr(100, 10, 130, 15, 'white')
        ds("<3\t" * vies[0], 88, 8, 'black')
        affiche()

Ici, le nombre de vies est affiché grâce à la variable globale vies. Entre autres, la fonction affiche() est appelée :

rouge = (249, 65, 68)
orange = (243, 114, 44)
vert = (144, 190, 109)

cases_retournees = []
cases_marquees = []

def affiche():
    for coord1 in cases_marquees:
        fr(71 + 35 * coord1[0], 32 + 35 * coord1[1], 35, 2, rouge)
        fr(71 + 35 * coord1[0], 32 + 35 * coord1[1], 2, 35, rouge)
        fr(71 + 35 * coord1[0], 64 + 35 * coord1[1], 35, 2, rouge)
        fr(103 + 35 * coord1[0], 32 + 35 * coord1[1], 2, 35, rouge)
    for coord2 in cases_retournees:
        if plateau_inf[coord2[0]][coord2[1]] == 'G':
            couleur = vert
        elif plateau_inf[coord2[0]][coord2[1]] == 'O':
            couleur = orange
        elif plateau_inf[coord2[0]][coord2[1]] == 'R':
            couleur = rouge
        elif plateau_inf[coord2[0]][coord2[1]] == 'S':
            couleur = 'grey'
        fr(71 + 35 * coord2[0], 32 + 35 * coord2[1], 34, 34, couleur)

Cette fonction, en premier lieu, parcourt la liste cases_marquees et ajoute un contour rouge à chaque case dont les coordonnées sont présentes dedans. Ce contour est composé de quatre rectangles fins qui forment un carré.

affiche() parcourt également la liste cases_retournees qui retient les cases dont il faut afficher la couleur si elles ne cachent pas un pique. En fonction du contenu du plateau caché, la fonction attribue une couleur différente à chaque case.

Cette première partie de la boucle se termine par l’affichage des nombres sur la grille. Cependant, la tâche n’est pas aussi aisée qu’en apparence. En effet, lorsqu’une ligne de texte est affichée grâce à la fonction draw_string() (représentée ici par « ds »), son fond est blanc par défaut.

Seulement, si une case est retournée et est donc colorée, afficher un chiffre sur fond blanc dessus est peu esthétique :

Il faut donc changer le fond de la ligne de texte affichée en fonction de la case sur laquelle elle se trouve :

def jeu():
    # [...]	
		for a in range(0, 5):
            for b in range(0, 5):
                couleur = 'white'
                if (a, b) in cases_retournees:
                    if plateau_inf[a][b] == 'G':
                        couleur = vert
                    elif plateau_inf[a][b] == 'O':
                        couleur = orange
                    elif plateau_inf[a][b] == 'R':
                        couleur = rouge
                    elif plateau_inf[a][b] == 'S':
                        couleur = 'grey'
                ds(str(plateau_sup[a][b]), 83 + 35 * a, 41 + 35 * b, 'black', couleur)

Les boucles bornées imbriquées l’une dans l’autre génèrent toutes les coordonnées possibles sur une grille de dimensions 5 x 5. Ensuite, on observe lesquelles de ces coordonnées correspondent à des cases retournées, et on colorie le fond du texte en fonction de la case.

Enfin, passons au cœur de la boucle. Il y a beaucoup d’éléments à analyser.

def jeu():
    # [...]	
		while True:
            curseur(x, y, jaune)
            x_bis, y_bis = x, y
            touche = attente([0, 1, 2, 3, 4, 52, 17])

La boucle infinie commence par afficher le curseur du joueur aux bonnes cordonnées grâce à la fonction curseur() :

def curseur(x, y, c):
    fr(70 + 35 * x, 31 + 35 * y, 35, 1, c)
    fr(70 + 35 * x, 31 + 35 * y, 1, 35, c)
    fr(70 + 35 * x, 66 + 35 * y, 35, 1, c)
    fr(105 + 35 * x, 31 + 35 * y, 1, 35, c)

Ce curseur est formé de quatre rectangles fins.

Ensuite, la boucle utilise la fonction attente() (tirée de ce script) afin d’enregistrer les touches poussées par le joueur :

def attente(touches):
    while True:
        for nb in touches:
            if keydown(nb):
                while keydown(nb):
                    True
                return nb

Pourquoi ne pas simplement utiliser la fonction keydown() fournie par le module ion ? C’est tout bonnement simple : on veut détecter la touche poussée par l’utilisateur lorsque celui-ci relâche la touche. En effet, en utilisant keydown(), si le joueur reste appuyé sur une touche, alors, avec la boucle non-bornée, le mouvement serait répété plusieurs fois. Cela rendrait l’expérience de jeu ingérable puisque le curseur se déplacerait sur la longueur ou largeur de la grille au moindre appui de touche. Il nous faut donc attendre que l’utilisateur relâche la touche, et c’est ce que nous permet la fonction attente().

En ce qui concerne le reste de la fonction jeu(), selon la touche poussée, on effectue différentes actions.

def jeu():
    # [...]	
			if touche == 4 or touche == 52:
                if (x, y) not in cases_retournees:
                    cases_retournees.append((x, y))
                    verif(x, y)
                    break

Dans le cas ou le joueur appuie sur la touche [OK], on s’assure que la case sur laquelle il se trouve n’est pas déjà retournée, et, dans ce cas, on la retourne. Enfin, on appelle la fonction verif() qui vérifie l’état de la partie, c’est-à-dire si le joueur a gagné, perdu, ou s’il continue de jouer :

def verif(x, y):
    if plateau_inf[x][y] == 'X':
        fr(80, 60, 160, 100, 'black')
        ds("Bien joué !", 110, 100, 'white', 'black')
        attente([4, 52, 17])
        menu()
    elif plateau_inf[x][y] == 'S':
        vies[0] -= 1
        if vies[0] < 1:
            fr(90, 10, 130, 15, 'white')
            fr(80, 60, 160, 100, 'black')
            ds("Perdu !", 130, 100, 'white', 'black')
            attente([4, 52, 17])
            menu()

Si le joueur a retourné la case qui cachait le trésor, on le ramène au menu ; si, en revanche, celui-ci tombe sur un pique, on lui retire une vie, puis, on jette un coup d’œil au nombre de vies qu’il lui reste, et s’il est à zéro, on ramène le joueur au menu. La fonction menu() sera présentée plus tard.

De retour à la boucle. Si le joueur appuie sur la touche [DEL], on s’assure que la case sur laquelle il se trouve n’est pas déjà marquée, et, dans ce cas, on la marque.

def jeu():
    # [...]	
		elif touche == 17:
            if (x, y) not in cases_marquees:
                cases_marquees.append((x, y))
            break

Les deux bouts de code de la boucle présentés contiennent tous deux le mot-clef « break ». Ce dernier permet de sortir de la boucle secondaire afin de revenir dans la boucle principale de sorte que l’écran puisse être « rafraîchi », c’est-à-dire que les sprites soient mis à jour.

Enfin, en fonction de la flèche de navigation poussée par le joueur, on change les coordonnées de son curseur si celui-ci ne s’évade pas de la grille, puis on colore en noir l’emplacement précédent du curseur.

def jeu():
    # [...]	
		elif x > 0 and touche == 0:
            x -= 1
        elif x < 4 and touche == 3:
            x += 1
        elif y > 0 and touche == 1:
            y -= 1
        elif y < 4 and touche == 2:
            y += 1
        curseur(x_bis, y_bis, 'black')

Et nous voilà à la fin de la fonction jeu(). Le plus dur est dernière nous !

La mise en place du menu principal

La fonction menu() permet d’afficher les différents seuils de difficulté et d’accéder au tutoriel. Cette fonction est la première a être appelée.

Tout comme la fonction jeu(), menu() est composée d’une initialisation et d’une boucle.

violet = (148, 113, 222)
choix = 0

def menu():
    global choix
    fr(0, 0, 320, 240, 'white')
    fr(0, 200, 320, 40, jaune)
    ds('Chasse au trésor', 80, 20, 'black')
    ds('-----------------', 75, 40, 'black')
    ds("Code by nsi.xyz/chasse-au-tresor", 0, 202,'white', violet)

Durant l’initialisation, on affiche tout simplement quelques lignes de texte telles que le titre du jeu. Rien de bien compliqué. Il serait cependant intéressant de faire remarquer que la variable choix est placée hors de la fonction avant d’être rendue globale grâce au mot-clef « global ».

Pourquoi donc ? Eh bien, puisque la variable est hors de la fonction, sa valeur n’est pas écrasée lorsque l’on appelle à nouveau la fonction. Ainsi, le jeu peut se souvenir du choix du joueur lorsque celui-ci navigue parmi les options.

Passons à présent à la boucle de la fonction :

choix_couleurs = {0: ('Facile', 128, 70, vert),
                  1: ('Moyen', 133, 100, orange),
                  2: ('Difficile', 115, 130, rouge),
                  3: ('Tutoriel', 118, 160, jaune)}

def menu():
    # [...]
	while True:
        for i in range(4):
            texte, x, y, couleur = choix_couleurs[i]
            ds(texte, x, y, couleur if i == choix else 'black')

Cette partie de la boucle n’est pas aussi compliquée qu’il n’y paraît. En effet, selon le choix du joueur, elle affiche les quatre options disponibles dans différentes couleurs en exploitant un dictionnaire. Cela permet donc à l’utilisateur de visualiser ce qu’il s’apprête à faire.

La seconde partie de la boucle est également simple. Si le joueur appuie sur la flèche du haut ou du bas, la variable choix est mise à jour. Les conditions supplémentaires aux lignes 4 et 6 permettent de ne pas sortir des quatre options possibles :

def menu():
    # [...]
        touche = attente([1, 2, 4, 52])   
            if touche == 1 and choix > 0:
                choix -= 1
            elif touche == 2 and choix < 3:
                choix += 1
            elif touche == 4 or touche == 52:
                if choix == 0:
                    nb_piques[0] = 11
                    jeu()
                if choix == 1:
                    nb_piques[0] = 13
                    jeu()
                if choix == 2:
                    nb_piques[0] = 15
                    jeu()
                if choix == 3:
                    tutoriel()

En revanche, si l’utilisateur pousse la touche [OK], on modifie le nombre de piques selon la difficulté choisie, puis on se rend au jeu ou tutoriel. Et voilà !

Mot de fin

Créer ce projet sous couche graphique sur la calculatrice a été une expérience enrichissante. Non seulement ai-je été obligé de bien réfléchir à la logique de mon jeu en accord avec celle de mon programme, mais j’ai également dû coder avec les contraintes de mémoire de la calculatrice.

Mon script, j’en suis certain, pourrait être bien davantage optimisé ; mais je pense avoir fait du mieux que j’ai pu dans le temps imparti.

Je tiens également à faire remarquer que je n’ai pas abordé la fonction tutoriel() étant donné que celle-ci n’est composée que de lignes de texte affichées par la fonction draw_string(), et n’est par conséquent pas des plus captivantes à analyser.

Merci d’avoir lu jusqu’ici !

Lien

Voici le lien du jeu qui amène à mon workshop :