Étiquette : NumWorks

Art

De l’art sur la NumWorks

Les calculatrices graphiques programmables sont bien plus que de simples outils pour les calculs. Elles ouvrent un monde de possibilités aux élèves, en permettant d’explorer les mathématiques d’une manière interactive et visuelle. Mais leur potentiel va au-delà des formules et des graphiques : elles peuvent être utilisées pour créer de l’art numérique, des animations, et même des jeux !

En programmant ces machines, les étudiants développent leur créativité tout en renforçant leurs compétences en logique et en résolution de problèmes. Ces calculatrices deviennent ainsi des alliées indispensables pour apprendre de manière ludique et innovante, et pour repousser les limites de l’imagination.

Depuis 2019, j’invite mes élèves à explorer leur potentiel créatif en réalisant des dessins uniques, soit avec l’application Grapheur, soit avec l’application Python de la calculatrice. Chaque année, leurs œuvres sont soumises à l’éditeur de la calculatrice, NumWorks, qui sélectionne la plus belle réalisation. L’auteur de cette création se voit offrir une coque au design original pour sa calculatrice. Si vous croisez un élève au lycée Louis Pasteur arborant une telle coque, il est fort probable qu’il soit lauréat de l’un de ces concours.

Certaines de ces réalisations sont également partagées sur les réseaux sociaux, notamment sur Instagram et X (anciennement Twitter). Une quinzaine de ces images ont même été sélectionnées et publiées, avec l’accord de leurs auteurs, dans un livre intitulé Découvrir la calculatrice graphique NumWorks édité en Français et en Anglais.

Il est vrai que se lancer dans la programmation sur une calculatrice graphique peut sembler déroutant au premier abord. Le défi de traduire des idées créatives en code peut déstabiliser, surtout pour ceux qui n’ont jamais exploré cet univers. Cependant, une fois les premières hésitations surmontées, les élèves découvrent rapidement le plaisir de voir leurs concepts prendre vie à l’écran. Ce processus, bien qu’exigeant, devient une source de satisfaction personnelle, les encourageant à se dépasser. En fin de compte, ils s’amusent tout en repoussant leurs propres limites, transformant un défi intimidant en une expérience enrichissante.

Tout à commencé en 2019 …

En 2019, après réflexion et concertation de l’équipe des enseignants de mathématiques, nous avons pris la décision collective de passer aux calculatrices NumWorks dans toutes nos classes. Pour familiariser les élèves avec cet outil, leur premier travail a été de réaliser un projet simple, destiné à leur faire découvrir les différentes fonctionnalités de la calculatrice. Sans attente particulière, j’ai été agréablement surpris par l’enthousiasme et la créativité dont ils ont fait preuve. Certaines productions, bien au-delà de ce que j’avais imaginé, ont révélé un potentiel insoupçonné chez les élèves, ouvrant la voie à des projets encore plus ambitieux par la suite.

Le titre du sujet était : Les mathématiques sont belles. Voici les 30 réalisations des élèves.

Certaines de ces images sont issues de scripts bien connus, adaptés et optimisés pour la NumWorks. Elles ont été réalisées à distance, en ligne, pendant le confinement de mars 2020. J’ai alors demandé à mes élèves de Terminale de constituer un jury pour évaluer ces créations, en insistant sur l’importance d’une bienveillance maximale dans leurs délibérations. Bien que les codes Python aient parfois été maladroits, ces erreurs faisaient partie intégrante du processus d’apprentissage. Se tromper, tâtonner, recommencer et s’améliorer sont autant d’étapes essentielles dans le développement de leurs compétences.

Quelques thread colorés publiés sur X (Twitter)

J’ai rejoint X (anciennement Twitter) durant le confinement, à l’origine pour passer le temps. Depuis, j’ai régulièrement partagé des threads mettant en avant les créations de mes élèves. En voici quelques exemples. (Un tableau à la fin de cet article propose davantage de liens.)

https://twitter.com/nsi_xyz/status/1380526907477884930
https://twitter.com/nsi_xyz/status/1578772799136890880
https://twitter.com/nsi_xyz/status/1593645740219416577
https://twitter.com/nsi_xyz/status/1770189028366709193
https://twitter.com/nsi_xyz/status/1714607831733707112
https://twitter.com/nsi_xyz/status/1802272199455187027

De l’art sur la NumWorks depuis 2020

Le tableau ci-dessous récapitule tous les fils de discussion publiés sur X (anciennement Twitter). Malheureusement, depuis le rachat de Twitter par Elon Musk, il n’est plus possible de consulter ces fils sans posséder un compte sur la plateforme. Cette restriction illustre bien les défis liés à l’utilisation d’outils fermés, qui peuvent rapidement se transformer en véritables obstacles à l’accès à l’information. 🥴

A compter novembre 2024, compte tenu de la fermeture progressive de X, de l’abus des publicités et de la toxicité du réseau, les fils seront publiés sur Blue Sky.
Les fils sur X existent toujours, en cliquant sur le T vous pouvez le lire sans compte sur X (Twitter)

Création
(1er éd.)
Titre du travail à rendre
Classe cible
Thread X (Twitter)
Blue Sky
2019
(2020)
Les mathématiques sont belles.
Seconde, mathématiques
2020 2021 2022 2023 2024 2025
2021
(2021)
Le Python et la Tortue.
1ère, spécialité NSI
2021 2022 2023 20242025
2021
(2021)
Le python et les fractales
Tale, spécialité NSI
2021 2022 2023 20242025
2022
(2022)
Les mathématiques sont belles ! ed. spéciale cercle
Tale, option Mathématiques Expertes
2022 2023 2024
2022
(2023)
Les mathématiques sont belles ! ed. spéciale polynôme
Tale, option Mathématiques Expertes
2023, 2024 20252025
2022
(2024)
Un feu d’artifice en python
Tale, option Mathématiques Expertes
2024 20252025
2023
(2023)
Le Python et la Tortue.
Seconde, SNT
2023 20242025
2024
(2024)
Pixel Art en python
Tale, option Mathématiques Expertes
20242025

Tutoriels

Comment mettre à jour sa calculatrice NumWorks ?

Vous venez d’acheter une calculatrice NumWorks ou vous en possédez déjà une et vous voulez comprendre comment la mettre à jour ? Alors ce tutoriel est fait pour vous : découvrez à travers une explication pas à pas comment mettre à jour votre calculatrice et bénéficiez des nouveautés disponibles sur celle-ci.

Trailer du tutoriel :

Pourquoi mettre à jour sa calculatrice ?

Tout d’abord, si vous venez d’acheter la calculatrice, elle ne possède pas la dernière version en date et peut ne pas posséder certaines fonctionnalités qui, pour certains, peuvent être essentielles dans leur utilisation de la calculatrice.

De plus, la calculatrice est sans cesse en évolution pour améliorer son utilisation. Des mises à jour sont donc régulièrement publiées. Il faut penser à les installer de temps en temps. Cela ne lui fera pas de mal.

La mise à jour étape par étape de votre calculatrice :

Pour que ce tutoriel soit simple et efficace, il vous expliquera les différentes étapes à réaliser pour réussir la mise à jour de votre calculatrice. Toutes les procédures doivent être réalisées sur un ordinateur pour faciliter le passage d’une étape à l’autre. Vous aurez également besoin de votre calculatrice et d’un câble USB (celui d’origine de préférence). Les captures d’écran du tutoriel ont été réalisées avec un ordinateur utilisant Windows et Google.

Voici les différentes étapes :

1ère étape : le compte NumWorks :

Dans cette première étape, vous allez devoir vous connecter à votre compte NumWorks. Si vous n’en avez pas encore un, cette étape explique également comment le créer.

Tout d’abord, rendez-vous sur numworks.com :

Puis, cliquez sur « Mon compte » :

Vous arriverez sur cette page :

Si vous avez déjà un compte, vous pouvez passer l’explication de l’inscription en cliquant ici.

Si vous ne possédez pas de compte, cliquez sur « S’inscrire » :

Vous arriverez sur cette page :

Pour créer votre compte, vous devrez alors remplir les informations demandées, c’est-à-dire donner votre adresse e-mail avec laquelle vous voulez créer votre compte et créer un mot de passe (d’au moins 6 caractères) que vous devrez écrire une deuxième fois pour le valider.

Après avoir rentré ces données, cliquez sur « S’inscrire » :

(L’adresse e-mail a été floutée, car c’est une donnée confidentielle. Normalement, la vôtre doit toujours apparaître sur votre écran.)

Après avoir cliqué, un message apparaîtra vous indiquant qu’un lien de confirmation a été envoyé à votre adresse e-mail.

Pour valider votre compte, vous devrez alors vous rendre dans votre boîte de réception et ouvrir l’e-mail :

(Si vous n’avez pas reçu l’e-mail, pensez à vérifier vos spams. Sinon, vous pouvez demander un nouveau lien de confirmation.)

Comme indiqué dans l’e-mail, cliquez sur le lien pour valider votre compte.

Vous arriverez donc sur une nouvelle page :

Vous pouvez alors remplir les informations demandées ou tout simplement cliquer sur « Sauvegarder » (ces informations sont facultatives) :

Vous arrivez donc sur cette page :

Étant déjà connecté(e), vous pouvez directement passer à la deuxième étape en cliquant ici.

Si vous possédez déjà un compte, cliquez sur « Se connecter » :

Après avoir cliqué, vous arriverez sur cette page :

Vous devez alors rentrer votre adresse e-mail et votre mot de passe, puis cliquer sur « Se connecter » :

(Si vous avez oublié votre mot de passe, vous pouvez cliquer sur « Mot de passe oublié » pour lancer la procédure de récupération de votre compte.)

Vous arrivez donc sur cette page si vous n’avez pas encore associé votre calculatrice :

Ou sur celle-ci, si vous avez déjà associé votre calculatrice au compte NumWorks :

Étant connecté(e), vous pouvez passer à la deuxième étape.

2ème étape : la connexion de la calculatrice :

Cette deuxième étape consistera à connecter votre calculatrice à votre ordinateur pour pouvoir ensuite l’associer à votre compte NumWorks.

Pour ceux qui ont déjà associé leur calculatrice à leur compte NumWorks, cliquez ici pour sauter les explications concernant l’ajout d’une calculatrice.

Sinon, si vous n’avez pas encore enregistré de calculatrice sur votre compte (ceux qui viennent de créer un compte ou ceux qui viennent de se connecter mais qui ne l’avaient jamais utilisé avant), vous arriverez donc, comme montré dans la première étape, sur la page ci-dessous.

Premièrement, cliquez sur « Mettre à jour mon appareil » :

Vous arriverez sur cette page :

Vous devrez ensuite cliquer sur « Mettre à jour » :

Après avoir cliqué, vous arriverez sur cette page :

Vous devez, comme c’est indiqué, brancher votre calculatrice à votre ordinateur avec de préférence le câble USB d’origine. Une fois le branchement effectué, cliquez sur « J’ai branché ma calculatrice » :

Vous arriverez sur cette page :

Vérifiez alors que la calculatrice affiche bien qu’elle est connectée, puis cliquez sur « J’ai vérifié que la calculatrice est connectée » :

(Si la calculatrice n’affiche rien, pensez à vérifier qu’elle est bien branchée, essayez de changer de port USB et de câble. Vous pouvez également éteindre puis rallumer la calculatrice puis essayer de la rebrancher.)

Vous êtes donc sur cette page :

Cliquez sur « Lancer la détection » :

Une nouvelle fenêtre apparaît :

Vous devez alors cliquer sur « NumWorks Calculator » puis sur « Connexion » :

Vous arrivez ensuite sur cette page :

Vous avez réussi à connecter votre calculatrice à votre ordinateur et à l’associer à votre compte NumWorks ! Vous pouvez à présent passer à la troisième étape en cliquant ici.

Si vous avez déjà associé votre calculatrice à votre compte NumWorks, vous avez dû arriver sur cette page à la fin de la première étape :

Vous devez donc cliquer sur « Mise à jour » :

Vous serez alors dirigés sur cette page :

Vous devrez ensuite cliquer sur « Mettre à jour » :

Après avoir cliqué, vous arriverez sur cette page :

Vous devez, comme c’est indiqué, brancher votre calculatrice à votre ordinateur avec de préférence le câble USB d’origine. Une fois le branchement effectué, cliquez sur « J’ai branché ma calculatrice » :

Vous arriverez sur cette page :

Vérifiez alors que la calculatrice affiche bien qu’elle est connectée, puis cliquez sur « J’ai vérifié que la calculatrice est connectée » :

(Si la calculatrice n’affiche rien, pensez à vérifier qu’elle est bien branchée, essayez de changer de port USB et de câble. Vous pouvez également éteindre puis rallumer la calculatrice puis essayer de la rebrancher.)

Vous arrivez ensuite sur cette page :

Vous avez réussi à connecter votre calculatrice à votre ordinateur ! Vous pouvez à présent passer à la troisième étape.

3ème étape : la mise à jour de la calculatrice :

Cette troisième et dernière étape vous permettra de mettre à jour votre calculatrice pour qu’elle soit prête à être utilisée dans les meilleures conditions possibles.

Tout d’abord, cliquez sur « Installer la mise à jour » :

Vous arriverez donc sur cette page :

(La barre orange montrant l’avancée de l’installation de la mise à jour sur votre calculatrice.)

Une fois l’installation terminée, cette page s’affichera :

Il ne vous reste plus qu’à débrancher la calculatrice, sélectionner votre langue et votre pays sur celle-ci et vous aurez fini la procédure de mise à jour !

NumApps

Découvrir la calculatrice graphique NumWorks

Fonctionnalités, exemples illustrés, le guide indispensable pour apprendre à maîtriser sa NumWorks. Un livre publié sur Amazon.

Bien débuter avec une calculatrice NumWorks

Que vous ayez déjà acheté une calculatrice graphique NumWorks ou que vous envisagiez de le faire, ce livre est fait pour vous.

Il propose une découverte des différentes applications de la NumWorks :
Calculs, Grapheur, Python, Statistiques, Probabilités, Équations, Suites, Régressions, Eléments, Inférence, Finance et Paramètres.

Chaque page contient des illustrations ainsi que les touches à presser pour arriver au résultat proposé.
Des travaux d’élèves sont inclus, espérons qu’ils stimuleront votre créativité.

Les 12 applications présentées à travers des exemples

ApplicationNombre de pagesTitre d’une page à titre d’exemple
1. Calculs16Calculer avec des taux d’évolution en %
2. Grapheur16Recherche d’antécédent, d’extremum, de zéro
3. Python16Générer un dégradé, Pixel Art
4. Statistiques8Comparer des séries statistiques
5. Probabilités8Loi normale
6. Équations8Équations du second degré
7. Suites8Exploiter une suite dans l’application calculs
8. Régressions8Calculer la droite de régression linéaire
9. Eléments8Recherche par numéro atomique
10. Inférence4Intervalle de confiance
11. Finance4Simuler un prêt bancaire de 200 000€ sur 20 ans à 3.5%
12. Paramètres8Police Python : grande ou petite, au choix !
Annexes4Le mode examen
Mettre à jour sa calculatrice
Quelques réalisations
A propos

Des fonctions et des scripts python à tester

A saisir ou à télécharger directement sur la calculatrice via des liens rapides.

Un guide pas à pas pour une découverte facilité

Découvrir la calculatrice graphique NumWorks

Avoir une calculatrice graphique c’est bien, mais maîtrisez-vous votre calculatrice ?
Partez à l’exploration de ses fonctionnalités, découvrez son ergonomie à travers un guide pas à pas illustré.

ASIN ‏ : ‎ B0CFCYW5ZK
Langue ‏ : ‎ Français
Broché ‏ : ‎ 120 pages
ISBN-13 ‏ : ‎ 979-8857164624
Dimensions ‏ : ‎ 15.24 x 0.71 x 22.86 cm

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 Puissance 4 sur ta NumWorks !

Le Puissance 4 est probablement un jeu de votre enfance, c’est pourquoi je vous propose de jouer à ma version du Puissance 4 directement sur votre NumWorks avec un ami.

L’idée

L’idée du Puissance 4 a été longuement réfléchi car le projet de ces vacances était libre et j’avais eu l’idée de faire un jeu sur la NumWorks mais je ne savais pas lequel faire. Après avoir parcouru le site à la recherche de jeu original qui n’avait pas été déjà fait, l’idée du Puissance 4 m’est venu à l’esprit.

La réalisation

Pour réaliser le jeu j’ai avant tout fait en sorte qu’il soit jouable sur la console avant de faire la partie graphique de la calculatrice (même si certains ajouts ont été fait pendant le développement de la partie graphique).

Le Script

Je vais maintenant vous présenter le script et vous faire une brève explication de chacune des fonctions :

from kandinsky import fill_rect as rect, draw_string as txt
from time import sleep
from ion import keydown

Au tout début du script nous avons évidemment les appels des différents modules :

  • Kandinsky : c’est un des modules propriétaire de NumWorks qui est utilisé afin de dessiner des rectangles ou même écrire du texte sur l’écran de la calculatrice
  • Time : permet avec la fonction sleep que j’appel de mettre des pause dans le script
  • Ion : c’est le second module propriétaire de NumWorks qui permet de prendre en compte l’appui des touches pendant le script.

Nous avons ensuite la définition des variables globales :

# 1 = rouge // 2 = jaune
# Variables globales
player = 1 #Définit le joueur qui doit jouer
grille_preview = [0, 0, 0, 0, 0, 0, 0] #Est utilisé pour déterminer les positions possible du preview
grille = [[0 for i in range(7)] for i in range(6)] #La matrice qui représente la grille de jeu

#Les couleurs utilisées
rouge = (182, 2, 5)
jaune = (255, 181, 49)
gris = (191, 189, 193)

pos = 3 #Donne la position du preview

#Les points des joueurs
points_rouge = 0
points_jaune = 0

Ensuite une des fonctions majeures du jeu : la fonction de vérification. Cette fonction va après chaque coup vérifier toutes les positions afin de voir si il y a un gagnant ou pas.

def verifie(): #Gagnant ?
  for i in range(6): #lignes
    for j in range(4):
      if grille[i][j] == player and grille[i][j+1] == player and grille[i][j+2] == player and grille[i][j+3] == player:
        gagnant(player)
  for i in range(3): #colonnes
    for j in range(7):
      if grille[i][j] == player and grille[i+1][j] == player and grille[i+2][j] == player and grille[i+3][j] == player:
        gagnant(player)
  for i in range(3): #diagonales
    for j in range(4):
      if grille[i][j] == player and grille[i+1][j+1] == player and grille[i+2][j+2] == player and grille[i+3][j+3] == player:
        gagnant(player)
  for i in range(3, 6):
    for j in range(4):
      if grille[i][j] == player and grille[i-1][j+1] == player and grille[i-2][j+2] == player and grille[i-3][j+3] == player:
        gagnant(player)

Cependant je n’ai pas tout dit à propos de cette fonction car celle ci n’est pas tout à fait de moi : en effet avant d’avoir cette fonction là j’avais codé une fonction vérification mais celle ci faisait environ 70 lignes et n’était pas du tout optimisé ce qui amenait à des ralentissement, ChatGPT m’a donc aidé à réduire la fonction afin d’avoir la version ci-dessus. Et donc voici en exclusivité ma fonction de vérification originelle :

def verifie():
    #horizontalement
    for i in range(6):
        for j in range(4):
            rouge = 0
            jaune = 0
            for k in range(4):
                if grille[i][j+k] == 1 :
                    rouge += 1
                elif grille[i][j+k] == 2 :
                    jaune += 1
            if rouge == 4 :
                player_won(1)
            elif jaune == 4 :
                player_won(2)
    
    #verticalement
    for i in range(3):
        for j in range(7):
            rouge = 0
            jaune = 0
            for k in range(4):
                if grille[i+k][j] == 1 :
                    rouge += 1
                elif grille[i+k][j] == 2 :
                    jaune += 1
            if rouge == 4 :
                player_won(1)
            elif jaune == 4 :
                player_won(2)

    #diagonalement
    """(ij)
    /
    2 diag de 4 : 03 12 21 30 // 26 35 44 53
    2 diag de 5 : 04 13 22 31 40 // 16 25 34 43 52
    2 diag de 6 : 05 14 23 32 41 50 // 06 15 24 33 42 51
    
    \
    2 diag de 4 : 03 14 25 36 // 20 31 42 53
    2 diag de 5 : 02 13 24 35 46 // 10 21 32 43 54
    2 diag de 6 : 00 11 22 33 44 55 // 01 12 23 34 45 56
    """

    #création des listes pour vérifier
    diag4 = [[[0, 3], [1, 2], [2, 1], [3, 0]], [[2, 6], [3, 5], [4, 4], [5, 3]], [[0,3], [1,4], [2,5], [3,6]], [[2,0], [3,1], [4,2], [5,3]]]
    diag5 = [[[0, 4], [1, 3], [2, 2], [3, 1], [4, 0]], [[1, 6], [2, 5], [3, 4], [4, 3], [5, 2]], [[0, 2], [1, 3], [2, 4], [3, 5], [4, 6]], [[1, 0], [2, 1], [3, 2], [4, 3], [5, 4]]]
    diag6 = [[[0, 5], [1, 4], [2, 3], [3, 2], [4, 1], [5,0]],[[0, 6], [1, 5], [2, 4], [3, 3], [4, 2], [5,1]],[[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5,5]],[[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5,6]]]

    #vérification des diagonales de 4
    for i in range(4):
        if grille[diag4[i][0][0]][diag4[i][0][1]] == grille[diag4[i][1][0]][diag4[i][1][1]] == grille[diag4[i][2][0]][diag4[i][2][1]] == grille[diag4[i][3][0]][diag4[i][3][1]] :
            if grille[diag4[i][0][0]][diag4[i][0][1]] == 1 :
                player_won(1)
            elif grille[diag4[i][0][0]][diag4[i][0][1]] == 2 :
                player_won(2)

    #vérification des diagonales de 5
    for i in range(4):
        for j in range(2):
            if grille[diag5[i][0+j][0]][diag5[i][0+j][1]] == grille[diag5[i][1+j][0]][diag5[i][1+j][1]] == grille[diag5[i][2+j][0]][diag5[i][2+j][1]] == grille[diag5[i][3+j][0]][diag5[i][3+j][1]] :
                if grille[diag5[i][0+j][0]][diag5[i][0+j][1]] == 1 :
                    player_won(1)
                elif grille[diag5[i][0+j][0]][diag5[i][0+j][1]] == 2 :
                    player_won(2)

    #vérification des diagonales de 6               
    for i in range(4):
        for j in range(3):
            if grille[diag6[i][0+j][0]][diag6[i][0+j][1]] == grille[diag6[i][1+j][0]][diag6[i][1+j][1]] == grille[diag6[i][2+j][0]][diag6[i][2+j][1]] == grille[diag6[i][3+j][0]][diag6[i][3+j][1]] :
                if grille[diag6[i][0+j][0]][diag6[i][0+j][1]] == 1 :
                    player_won(1)
                elif grille[diag6[i][0+j][0]][diag6[i][0+j][1]] == 2 :
                    player_won(2)

Ma fonction donc vérifiait chaque possibilité afin de gagner au jeu mais le plus long était surtout les diagonales qui n’étaient pas automatisées avec des boucles sans variables définies mais plutôt avec des listes interminables des positions possibles des diagonales et cela ralentissait considérablement le script. C’est pour cela que j’ai préféré utilisé la fonction de ChatGPT.

Par la suite nous avons une autre fonction majeure qui est celle qui est appelée dans le cas d’une fin de partie donc soit ci un joueur a gagné ou si il y a égalité et c’est cette même fonction qui redémarre une partie donc rénitialise la grille et actualise les scores :

def gagnant(winner): #Met fin à une partie gagnant ou pas
  global points_jaune, points_rouge, player, nb_partie, grille, partie
  affichage_grille()
  if winner == 1 : #Vérifie la variable winner pour savoir qui a gagné et agir en conséquence
    points_rouge += 1
    player = 2
    txt("Rouge a gagné !", 88, 20)
  elif winner == 2 :
    points_jaune += 1
    player = 1
    txt("Jaune a gagné !", 88,20)
  else :
    txt("C'est une égalité", 76, 20)
  wait() #La fonction attend qu'il y ai une touche de pressé (permet de voir la grille avant sa rénitialisation
  actu_src() #Actualisation des score
  grille = [[0 for i in range(7)] for i in range(6)] #Rénitialise la grille
  affichage_grille() #Affiche la grille

Ensuite nous avons la fonction selection qui est je dirai la dernière fonction majeure du script, elle permet au joueur de selectionner la colonne ou il souhaite ajouter son jeton et appel la fonction jouer que nous allons voir juste après :

def selection():
  global pos
  affichage_grille()
  preview(pos) #Affiche le preview du jeton (par défaut la 4ème colonne)
  add = 1
  while True : # Boucle qui permet de prendre en compte l'appui des touches et agit en conséquence.
    preview(pos)
    while colonne_pleine(pos):
      pos = (pos+add)%7
      preview(pos)
    key_pressed = wait() #Récupère la touche appuyé
    if key_pressed == 0: #Flèche de gauche // décale le preview vers la gauche
      add = -1
    if key_pressed == 3:#Flèche de droite // décale le preview vers la droite
      add = +1
    if key_pressed==0 or key_pressed==3: #Calcul la position du preview 
      pos = (pos+add)%7
      preview(pos)
    if key_pressed == 4 or key_pressed == 52 : #OK ou EXE (permet de jouer le coup)
      rect(75, 17, 170, 20, (255,255,255))
      jouer(pos)

Suite à cela il y a la fonction jouer qui est appelé dans la fonction selection et qui permet de jouer le coup dans la colonne sélectionné dans selection() :

def jouer(colonne): # Ajoute un jeton dans la colonne donnée
  global player
  if player == 1 : #Si c'est au tour de rouge
    animation(colonne) # Fais l'animation de chute du jeton
    grille[gravite(colonne)][colonne] = 1 #Modifie la matrice 
    verifie() #Vérifie si il y a un gagant
    player = 2 #Change de joueur
  elif player == 2 : #La même chose que pour le rouge mais pour le jaune
    animation(colonne)
    grille[gravite(colonne)][colonne] = 2
    verifie()
    player = 1
  grille_pleine() #Vérifie si la grille est pleine dans le cas d'un nul

Après il y a la fonction qui anime la chute du jeton dans la grille :

def animation(colonne): #Animation chute
  if player == 1 : #Définit la couleur du jeton
    color = rouge
  else :
    color = jaune
  for i in range(0, gravite(colonne)+1): #Dessine des jeton à la suite jursqu'à la dernière ligne
      ligne = (i-1)*(i!=0)
      rect(75+(25*colonne),42+(ligne*25),20,20,gris)
      sleep(0.05)
      draw_cercle(75 + (25*colonne) + 7, 42 + (i*25) + 2, color)
      sleep(0.05)

Ensuite la fonction qui dessine les cercles mais qui n’est pas de moi mais de M.Robert :

def draw_cercle(x,y,color): #Fait des cercles (Par VR)
  for d in range(6):
    rect(x-d+(d==0),y+d+(d==5),6+2*d-2*(d==0),16-2*d-2*(d==5), color)

Par la suite il y a ma fonction colonne_pleine qui dit si la colonne donnée en paramètre est pleine ou pas :

def colonne_pleine(colonne):
  if (grille[0][colonne] == 1) or (grille[0][colonne] == 2):
    return True
  return False

Une fonction similaire à la précedente : fonction grille_pleine utilise colonne_pleine afin de déterminer si la grille est pleine ou pas donc si il y a égalité :

def grille_pleine(): #Vérifie si il y a une égalité soit si la grille est pleine
  colonne_pleines = 0
  for i in range(7): #Passe en revue les premières lignes de chaque colonne
    if colonne_pleine(i):
      colonne_pleines += 1
  if colonne_pleines == 7: #Si toutes les colonnes sont pleine
    gagnant(0) #Appelle la fonction gagnant pour mettre fin à la partie

Il y a aussi la fonction gravité qui va retourner la ligne la plus basse où le jeton peut aller :

def gravite(colonne): #Détermine la ligne où le jeton peut se placer
  ligne = 5
  while grille[ligne][colonne] != 0: #parcours la colonne de haut en bas jusqu'à trouver la ligne vide la plus basse 
    if ligne == 0 :
      return ligne
    ligne -= 1
  return ligne

Une autre des fonctions principales : affichage_grille permet comme son nom l’indique d’afficher la grille entière :

def affichage_grille():
  rect(75, 42, 175, 150, (255,255,255))
  pos_x, pos_y_base, marge = 50, 42, 25
  for i in range(7):
    pos_x += marge
    pos_y = pos_y_base
    for y in range(6):
      cote = 20
      if grille[y][i] == 1:
        color = rouge
      elif grille[y][i] == 2:
        color = jaune
      else :
        color = gris
      if grille[y][i] == 1 or grille[y][i] == 2:
        rect(pos_x, pos_y, cote, cote, gris)
        draw_cercle(pos_x + 7, pos_y + 2,color)
      else:
        rect(pos_x, pos_y, cote, cote, gris)
      pos_y += marge

La fonction efface une possible grille avec un rectange blanc de la même taille que la grille puis réaffiche la grille

Voici ce que produit la fonction :

Ensuite il y a affichage_preview qui va afficher le preview du jeton au bon endroit :

def affichage_preview(col_preview):
   rect(75, 17, 170, 20, (255,255,255)) #Efface l'ancien preview
   if player == 1 : #Choisi la bonne couleur
      color = rouge
   else :
      color = jaune
   draw_cercle(75 + (col_preview * 25) + 7, 17+2, color) #Fait le preview au bon endroit

Il y a la fonction wait qui retourne la touche sur laquelle vous appuyez :

def wait(buttons=(0,1,2,3,4,52)): #Retourne la touche appuyée
   while True:
      for i in buttons:
         if keydown(i):
            while keydown(i): True
            return i

Et enfin les deux dernières fonctions qui sont liées : les fonctions du score, il y a la fonction affichage_src qui est appelé au début du jeu afin d’afficher le score :

#Score par Thomas S. mais code par Robin C.
def affichage_src():
   txt("J-1", 22, 42)
   draw_cercle(35,70,rouge)
   txt("Score", 12, 117)
   if points_rouge < 10 :
      largeur = 32
   else :
      largeur = 27
   txt(str(points_rouge),32,142)
   txt("J-2", 267, 42)
   draw_cercle(280,70,jaune)
   txt("Score", 257, 117)
   if points_jaune < 10 :
      largeur = 277
   else :
      largeur = 272
   txt(str(points_jaune),277,142)

def actu_src(): #Actualise le score
   txt(str(points_rouge),37-5*len(str(points_rouge)),142)
   txt(str(points_jaune),282-5*len(str(points_jaune)),142)

Et il y a aussi la fonction actu_src qui va à chaque fin de partie, actualiser le score en écrasant l’ancien score.

Voici ce que produit la fonction affichage_src() seule :

Et pour finir il y a le lancement du jeu à la fin avec juste avant le code qui permet d’afficher le lien en bas de l’écran :

#Lien article
rect(0,200,320,22,(148,113,222))
txt("Code by nsi.xyz/puissance4",33,202,(242,)*3,(148,113,222))

#lancement du jeu
affichage_src()
selection()

Images du jeu

Conclusion

Pour conclure le tout, ce projet a été très plaisant à faire et très instructif. Il faut savoir que faire un jeu sur la NumWorks comme le mien demande du travail notamment sur la prise en main de la partie graphique qui n’a pas été sans problèmes pour moi mais avec les entrainements de M.Robert sur twitter la prise en main fut plus simple.

Lien du jeu

Il existe deux liens pour le jeu, sachez que seul le premier garanti de disposer de la dernière version et que le deuxième est un lien alternatif qui peut être dépassé.

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 :

NumApps

Une chasse au trésor sur la NumWorks

Explorez un plateau et déterrez le trésor caché tout en évitant les pièges dans ce jeu inspiré du démineur !

Règles

Le jeu se déroule sur une grille de dimensions 5 x 5 remplie de chiffres. Celle-ci cache des pics et un trésor à découvrir afin de gagner.

Chaque chiffre représente le nombre de cases adjacentes qui cachent un pique. Cela signifie que chaque chiffre peut s’étendre de 0 à 4 (les cases en diagonale ne sont pas comptées comme adjacentes, contrairement au démineur).

Vous pouvez choisir de retourner la case que vous désirez. Si la case retournée abrite le trésor, vous gagnez la partie. En revanche, si elle cache un pique (représenté par la couleur grise), vous perdez une vie. Lorsque vos trois vies sont épuisées, vous avez échoué.

Dans le cas où la case est vide, celle-ci affiche une couleur qui vous renseigne sur sa distance par rapport au trésor :

  • Une case verte est éloignée du trésor ;
  • Une case orange est à deux à quatre cases du trésor ;
  • Une case rouge est à une à deux cases du trésor.

Voyez le schéma ci-dessous :

Toutefois, ne vous laissez pas berner ! Parmi les cases colorées, trois sont fausses et indiquent la mauvaise couleur. La disposition des couleurs peut davantage ressembler à cela :

Ainsi, votre but est de découvrir suffisamment de cases colorées afin de pouvoir estimer la position du trésor. Il se peut tout à fait que vous retourniez la case gagnante du premier coup, donc n’hésitez pas à faire plusieurs parties !

Commandes

◁ ▷OKDEL
Naviguer dans les menus
Déplacer le curseur
Sélectionner une option
Retourner une case
Marquer une case*
Retour

* Marquer une case, contrairement au démineur, n’empêche pas de la retourner. Cette fonctionnalité sert simplement d’aide visuelle au joueur. Également, une fois qu’une case est marquée, on ne peut plus retirer la marque.

Captures d’écran

Pour plus d’informations

Jetez un coup d’œil à cet article si vous souhaitez en apprendre plus sur le développement du jeu !

Lien

Voici le lien vers le jeu depuis mon workshop :

Projets

Comment a été créé Pong

Dans cet article, nous allons nous interresser à la programmation et à l’architecture de la version 2 de Pong. Le jeu est écrit en Python pour numworks sous Oméga, car cette version offre plus de tas (100ko contre 32ko dans la version officielle).

Pourquoi une seconde version ?

La première version avait une architecture assez peu soignée car je ne l’avais pas pensé de manière globale. De plus, elle avais des manques de compression assez flagrants car elle fesait plus de 12ko. Pour le petite anecdote, j’avais du la compresser énormément pour pouvoir la faire tourner car elle souffrait d’un dépassement de mémoire. Paradoxalement, c’est un de mes jeux qui a le mieux marché, sans doute car il était très complet. Mais je ne voulais pas laisser à un programmeur qui tombe sur le jeu l’image que c’est un travail que je trouve a posteriori presque bâclé. J’ai donc entrepris de le réécrire.

Première étape : programmer les menus.

Moteur de menu

Pour une fois, j’ai voulu commencer par programmer le menu (j’ai appelé le moteur menulib). Je pourrais alors adapter l’architecture du moteur de jeu à l’architecture globale. Pour programmer ce moteur de menu, je suis parti d’une syntaxe de création de menu un peu alambiquée, qui rassemble touts les éléments iterractifs (le moteur ne s’occupe que des éléments interractifs, les titres ou autre doivent être dessinés à part): l’unique fonction prend en parametre un liste de tupple qui contiennent eux même le type, le titre et si besoin les valeurs de l’élément :

menu ( position x, position y, elements list , [color] , [background color] )

Les éléments on chacun un identifiant :

TypeIDSyntaxeInformation
Button« btn »["btn","name"]Simple button. Not intended to be used as a text
List« lst »["lst","name",("element1","element2"),current element index]Element that allow the user making a choice beetwen the different elements purposed
Slider« sld »["sld","name",(min val,max val),current value]Element that allow the user chosing a value bettwen min val and max val
(Extrait de la documentation, que j’avais écrite en anglais)

La fonction retourne alors le nom du bouton sélectionné et l’indexe de chaque liste ou slider.

De cette manière, j’ai pu créer facilement des menus et récupérer le résultat de l’interaction avec l’utilisateur. Exemple ci dessous avec le menu principal

def mainMenu(conf=base_conf):
  transition(conf) #petite animation

  drawSprite(LOGO,10,10,12,conf[Col2]) #dessiner le logo
  drawRect(10,80,300,142,conf[Col3]) #dessiner le fond
  
  act_el=[["btn","Play"],["lst","Mode",("Solo","2 Player","Vs Comp."),conf[Mode]],["btn","Game Options"],["btn","Graphics Options"]]
  #défini les éléments
  get_act=menu(20,100,act_el,conf[Col1],conf[Col3]) # appelle la fonction et affecte le résulat de l'action à get_act
  
  conf[Mode]=get_act[1]["Mode"] #défini la configuration du mode de jeu à la sélection.
  if get_act[0]=="Game Options":gameMenu(conf) #analyse quel bouton a été selectionné
  elif get_act[0]=="Graphics Options":graphMenu(conf)
  else:gameEngine(conf)

La configuration et la persistance des options

Vous avez peut-être remarqué que les fonctions mainMenu, graphMenu, gameMenu et transition prennent toutes en paramètre la variable conf. C’est une liste, en quelque sorte un fichier de configuration, qui contient toutes le informations et paramètres du programme. Cette liste est enregistrée dans un fichier sous forme de chaine grâce à la fonction str() elle est décompressé à partir de ce fichier de manière totalement transparente grâce à la fonction eval() :

def saveConf(conf):
  try : #fonction try utilisée pour éviter des arrêts suite à une potentielle erreur.
    with open("pong.conf","w") as f:
      f.truncate(0) #on vide le fichier ...
      f.write(str(conf)) #... et on réécrit la chaine
  except: print("Saving configuration failed.") #message d'erreur en cas d'échec, mais cela ne stop pas le programme

def loadConf():
  try: #fonction try utilisée pour éviter des arrêts suite à une potentielle erreur ou si la configuration n'a pas encore été enregistrée
    with open("pong.conf","r") as f:return eval(f.readline()) #comme la chaine correspond à une liste, elle est évaluée comme une liste par eval()
  except: #si le fichier n'existe pas
    print("Loading configuration failed.")
    return base_conf #on retourne la configuration par défaut

Pour plus de lisibilité du code, chaque élément de la configuration (qui rappelons-le, est une liste) n’est non pas atteint grace à un index mais grace à une constante qui a pour valeur l’index correspondant :

Mode,Diff,MaxPts,BallSpd,BallDetails,PadDetails,Col1,Col2,Col3,BgCol,Theme,Best=0,1,2,3,4,5,6,7,8,9,10,11

Gestion des thème

Pour gérer les theme, j’ai simplement utilisé la configuration. Les couleurs et le numéro du thème y sont contenus et mis à jour quand cela est nécessaire. Pour mettre à jour ces couleurs, une fonction récupère le numéro du thème dans la configuration et renvoie cette configuration avec les bonnes couleurs :

def setTheme(conf,nb):
  conf[Theme]=nb #définit le numéro du thème dans la configuration
  a,b,c,d=(255,255,255),(255,200,0),(100,100,100),(60,60,60) #défini les 4 couleurs du thème de base
  if nb==1:a,b,c,d=d,(200,150,60),(200,200,200),a #thème light : réarrange et modifie les couleurs
  elif nb==2:b=(220,50,50) #theme omega
  elif nb==3:b=(200,100,200) #theme nsiOs
  conf[Col1:BgCol+1]=a,b,c,d #on defini les couleurs dans la config
  return conf #et on renvoie la config

Le moteur de jeu

Architecture et déplacement

J’ai longtemps hésité sur le choix de l’architecture. J’ai bien évidemment directement pensé à une classe en orienté objet qui pourrait s’occuper à la fois des pads et de la balle. Mais cette option est assez très gourmande en mémoire et j’ai aussi pensé à une fonction « constructrice » qui retournerais une liste avec toutes les information de l’objet. Finallement, j’ai tout de même opté pour une classe Entity() qui contient différentes fonctions très utiles :

class Entity():
    def __init__(it,x,y,w,h,col,bg_col):
      #Fonction d'initialisation des attributs
      #Défini la position, la taille, la couleur, etc...
      it.x,it.y,it.w,it.h,it.col,it.bg_col=x,y,w,h,col,bg_col
      it.spd_x,it.spd_y=0,0
      it.last_draw=(int(it.x-it.w//2),int(it.y-it.h//2),int(it.w),int(it.h),it.bg_col)
    def hitBox(it,it2):
      #Fonction qui renvoie vrai si it (objet 1) touche it2 (objet 2)
      if it.x-it.w//2<it2.x+it2.w//2 and it.x+it.w//2>it2.x-it2.w//2 and it.y-it.h//2<it2.y+it2.h//2 and it.x+it.w>it2.x and it.y<it2.y+it2.h//2 and it.y+it.h//2>it2.y-it2.h//2:return True
      else: return False
    def applyVec(it):
      #Fonction qui applique la vitesse de l'objet à sa position
      it.x+=it.spd_x
      it.y+=it.spd_y
    
    def hideObj(it):
      #Fonction qui dessine un rectangle de la couleur du fond pour cacher le dernier affichage de l'objet
      drawRect(*it.last_draw)
    
    def drawObj(it,detail=0):
      #Fonction qui dessine un rectangle à la place de l'objet
      it.last_draw=[int(it.x-it.w//2),int(it.y-it.h//2),int(it.w),int(it.h),it.bg_col]
      if detail:
        for x2,y2 in zip((2,0),(0,2)):drawRect(int(it.x-it.w//2)+x2,int(it.y-it.h//2)+y2,int(it.w)-x2*2,int(it.h)-y2*2,it.col)
      else: drawRect(*it.last_draw[0:4]+[it.col])

Ensuite, j’ai amélioré le déplacement de la balle. Je lui ai ajouté un attribut ball.a qui représente son angle, et à chaque collision sa vitesse latérale et horizontale est redéfinie grace à une fonction :

def vec(s,a): #la fonction prend en parametre la vitesse s et l'angle a
      a=radians(a)#les fonctions trigo prennent l'angle en radians
      x=s*cos(a)
      y=s*sin(a)
      return x,y #retour du vecteur vitesse en 2D

Pour les collisions : c’est encore plus simple. J’utilise une fonction qui prend en paramètre l’angle de l’objet et de la surface, qui calcule l’angle d’incidence et qui retourne l’opposé :

def collide(a1,a2):
  return a2-simp(a1-a2)
#la fonctio simp() simplifie un angle pour qu'il soit compris entre 0° et 360°.

Apres, je « n’avais qu’a » associer des touches aux pad, gérer leur collision avec le haut et la bas et détecter quand la balle est en dehors.

Afficher le score

Il ne restait plus qu’a afficher le score (bon, en fait il restait pas mal de petits truc à faire mais je vais pas les détailler ici). Pour cela, j’ai utilisé la technique de Schraf : Math-infos que j’ai adapté. J’ai d’abord créé une fonction drawSprite() un peu compliquée, qui grosso modo lit un chiffre pour le décompresser en binaire; et lit chaque bit. Si le bit est de 1, un carré est dessiné. Si le bit est de 0, on laisse la case vide. La fonction drawNumber permet d’automatiser l’affichage d’un sprite pour chaque chiffre du nombre désiré.